mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/master' into reminder_refactoring
This commit is contained in:
commit
a0e1a1a1f9
@ -83,13 +83,14 @@ jobs:
|
|||||||
- checkout
|
- checkout
|
||||||
- run: 'go get -u github.com/alecthomas/gometalinter'
|
- run: 'go get -u github.com/alecthomas/gometalinter'
|
||||||
- run: 'go get -u github.com/tsenart/deadcode'
|
- run: 'go get -u github.com/tsenart/deadcode'
|
||||||
|
- run: 'go get -u github.com/jgautheron/goconst/cmd/goconst'
|
||||||
- run: 'go get -u github.com/gordonklaus/ineffassign'
|
- run: 'go get -u github.com/gordonklaus/ineffassign'
|
||||||
- run: 'go get -u github.com/opennota/check/cmd/structcheck'
|
- run: 'go get -u github.com/opennota/check/cmd/structcheck'
|
||||||
- run: 'go get -u github.com/mdempsky/unconvert'
|
- run: 'go get -u github.com/mdempsky/unconvert'
|
||||||
- run: 'go get -u github.com/opennota/check/cmd/varcheck'
|
- run: 'go get -u github.com/opennota/check/cmd/varcheck'
|
||||||
- run:
|
- run:
|
||||||
name: run linters
|
name: run linters
|
||||||
command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
|
command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=goconst --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
|
||||||
- run:
|
- run:
|
||||||
name: run go vet
|
name: run go vet
|
||||||
command: 'go vet ./pkg/...'
|
command: 'go vet ./pkg/...'
|
||||||
|
@ -6,12 +6,15 @@
|
|||||||
|
|
||||||
### Minor
|
### Minor
|
||||||
|
|
||||||
|
* **Provisioning**: Dashboard Provisioning now support symlinks that changes target [#12534](https://github.com/grafana/grafana/issues/12534), thx [@auhlig](https://github.com/auhlig)
|
||||||
* **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon)
|
* **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon)
|
||||||
* **Tags**: Default sort order for GetDashboardTags [#11681](https://github.com/grafana/grafana/pull/11681), thx [@Jonnymcc](https://github.com/Jonnymcc)
|
* **Tags**: Default sort order for GetDashboardTags [#11681](https://github.com/grafana/grafana/pull/11681), thx [@Jonnymcc](https://github.com/Jonnymcc)
|
||||||
* **Prometheus**: Label completion queries respect dashboard time range [#12251](https://github.com/grafana/grafana/pull/12251), thx [@mtanda](https://github.com/mtanda)
|
* **Prometheus**: Label completion queries respect dashboard time range [#12251](https://github.com/grafana/grafana/pull/12251), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Prometheus**: Allow to display annotations based on Prometheus series value [#10159](https://github.com/grafana/grafana/issues/10159), thx [@mtanda](https://github.com/mtanda)
|
* **Prometheus**: Allow to display annotations based on Prometheus series value [#10159](https://github.com/grafana/grafana/issues/10159), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212)
|
* **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212)
|
||||||
* **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon)
|
* **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon)
|
||||||
|
* **Dashboard**: Prevent auto refresh from starting when loading dashboard with absolute time range [#12030](https://github.com/grafana/grafana/issues/12030)
|
||||||
|
* **Templating**: New templating variable type `Text box` that allows free text input [#3173](https://github.com/grafana/grafana/issues/3173)
|
||||||
|
|
||||||
# 5.3.0 (unreleased)
|
# 5.3.0 (unreleased)
|
||||||
|
|
||||||
|
17
build.go
17
build.go
@ -22,6 +22,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
windows = "windows"
|
||||||
|
linux = "linux"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
|
//versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
|
||||||
goarch string
|
goarch string
|
||||||
@ -110,13 +115,13 @@ func main() {
|
|||||||
case "package":
|
case "package":
|
||||||
grunt(gruntBuildArg("build")...)
|
grunt(gruntBuildArg("build")...)
|
||||||
grunt(gruntBuildArg("package")...)
|
grunt(gruntBuildArg("package")...)
|
||||||
if goos == "linux" {
|
if goos == linux {
|
||||||
createLinuxPackages()
|
createLinuxPackages()
|
||||||
}
|
}
|
||||||
|
|
||||||
case "package-only":
|
case "package-only":
|
||||||
grunt(gruntBuildArg("package")...)
|
grunt(gruntBuildArg("package")...)
|
||||||
if goos == "linux" {
|
if goos == linux {
|
||||||
createLinuxPackages()
|
createLinuxPackages()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,7 +383,7 @@ func ensureGoPath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func grunt(params ...string) {
|
func grunt(params ...string) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == windows {
|
||||||
runPrint(`.\node_modules\.bin\grunt`, params...)
|
runPrint(`.\node_modules\.bin\grunt`, params...)
|
||||||
} else {
|
} else {
|
||||||
runPrint("./node_modules/.bin/grunt", params...)
|
runPrint("./node_modules/.bin/grunt", params...)
|
||||||
@ -420,7 +425,7 @@ func build(binaryName, pkg string, tags []string) {
|
|||||||
binary = fmt.Sprintf("./bin/%s", binaryName)
|
binary = fmt.Sprintf("./bin/%s", binaryName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if goos == "windows" {
|
if goos == windows {
|
||||||
binary += ".exe"
|
binary += ".exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,11 +489,11 @@ func clean() {
|
|||||||
|
|
||||||
func setBuildEnv() {
|
func setBuildEnv() {
|
||||||
os.Setenv("GOOS", goos)
|
os.Setenv("GOOS", goos)
|
||||||
if goos == "windows" {
|
if goos == windows {
|
||||||
// require windows >=7
|
// require windows >=7
|
||||||
os.Setenv("CGO_CFLAGS", "-D_WIN32_WINNT=0x0601")
|
os.Setenv("CGO_CFLAGS", "-D_WIN32_WINNT=0x0601")
|
||||||
}
|
}
|
||||||
if goarch != "amd64" || goos != "linux" {
|
if goarch != "amd64" || goos != linux {
|
||||||
// needed for all other archs
|
// needed for all other archs
|
||||||
cgo = true
|
cgo = true
|
||||||
}
|
}
|
||||||
|
1
devenv/docker/ha_test/.gitignore
vendored
Normal file
1
devenv/docker/ha_test/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
grafana/provisioning/dashboards/alerts/alert-*
|
137
devenv/docker/ha_test/README.md
Normal file
137
devenv/docker/ha_test/README.md
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
# Grafana High Availability (HA) test setup
|
||||||
|
|
||||||
|
A set of docker compose services which together creates a Grafana HA test setup with capability of easily
|
||||||
|
scaling up/down number of Grafana instances.
|
||||||
|
|
||||||
|
Included services
|
||||||
|
|
||||||
|
* Grafana
|
||||||
|
* Mysql - Grafana configuration database and session storage
|
||||||
|
* Prometheus - Monitoring of Grafana and used as datasource of provisioned alert rules
|
||||||
|
* Nginx - Reverse proxy for Grafana and Prometheus. Enables browsing Grafana/Prometheus UI using a hostname
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Build grafana docker container
|
||||||
|
|
||||||
|
Build a Grafana docker container from current branch and commit and tag it as grafana/grafana:dev.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cd <grafana repo>
|
||||||
|
$ make build-docker-full
|
||||||
|
```
|
||||||
|
|
||||||
|
### Virtual host names
|
||||||
|
|
||||||
|
#### Alternative 1 - Use dnsmasq
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ sudo apt-get install dnsmasq
|
||||||
|
$ echo 'address=/loc/127.0.0.1' | sudo tee /etc/dnsmasq.d/dnsmasq-loc.conf > /dev/null
|
||||||
|
$ sudo /etc/init.d/dnsmasq restart
|
||||||
|
$ ping whatever.loc
|
||||||
|
PING whatever.loc (127.0.0.1) 56(84) bytes of data.
|
||||||
|
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.076 ms
|
||||||
|
--- whatever.loc ping statistics ---
|
||||||
|
1 packet transmitted, 1 received, 0% packet loss, time 1998ms
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Alternative 2 - Manually update /etc/hosts
|
||||||
|
|
||||||
|
Update your `/etc/hosts` to be able to access Grafana and/or Prometheus UI using a hostname.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cat /etc/hosts
|
||||||
|
127.0.0.1 grafana.loc
|
||||||
|
127.0.0.1 prometheus.loc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Start services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Browse
|
||||||
|
* http://grafana.loc/
|
||||||
|
* http://prometheus.loc/
|
||||||
|
|
||||||
|
Check for any errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker-compose logs | grep error
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scale Grafana instances up/down
|
||||||
|
|
||||||
|
Scale number of Grafana instances to `<instances>`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ docker-compose up --scale grafana=<instances> -d
|
||||||
|
# for example 3 instances
|
||||||
|
$ docker-compose up --scale grafana=3 -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test alerting
|
||||||
|
|
||||||
|
### Create notification channels
|
||||||
|
|
||||||
|
Creates default notification channels, if not already exists
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./alerts.sh setup
|
||||||
|
```
|
||||||
|
|
||||||
|
### Slack notifications
|
||||||
|
|
||||||
|
Disable
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./alerts.sh slack -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable and configure url
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./alerts.sh slack -u https://hooks.slack.com/services/...
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable, configure url and enable reminders
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./alerts.sh slack -u https://hooks.slack.com/services/... -r -e 10m
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provision alert dashboards with alert rules
|
||||||
|
|
||||||
|
Provision 1 dashboard/alert rule (default)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./alerts.sh provision
|
||||||
|
```
|
||||||
|
|
||||||
|
Provision 10 dashboards/alert rules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./alerts.sh provision -a 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Provision 10 dashboards/alert rules and change condition to `gt > 100`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./alerts.sh provision -a 10 -c 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pause/unpause all alert rules
|
||||||
|
|
||||||
|
Pause
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./alerts.sh pause
|
||||||
|
```
|
||||||
|
|
||||||
|
Unpause
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ./alerts.sh unpause
|
||||||
|
```
|
156
devenv/docker/ha_test/alerts.sh
Executable file
156
devenv/docker/ha_test/alerts.sh
Executable file
@ -0,0 +1,156 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
requiresJsonnet() {
|
||||||
|
if ! type "jsonnet" > /dev/null; then
|
||||||
|
echo "you need you install jsonnet to run this script"
|
||||||
|
echo "follow the instructions on https://github.com/google/jsonnet"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://admin:admin@grafana.loc/api/alert-notifications/1)
|
||||||
|
if [ $STATUS -eq 200 ]; then
|
||||||
|
echo "Email already exists, skipping..."
|
||||||
|
else
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Email",
|
||||||
|
"type": "email",
|
||||||
|
"isDefault": false,
|
||||||
|
"sendReminder": false,
|
||||||
|
"uploadImage": true,
|
||||||
|
"settings": {
|
||||||
|
"addresses": "user@test.com"
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
http://admin:admin@grafana.loc/api/alert-notifications
|
||||||
|
fi
|
||||||
|
|
||||||
|
STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://admin:admin@grafana.loc/api/alert-notifications/2)
|
||||||
|
if [ $STATUS -eq 200 ]; then
|
||||||
|
echo "Slack already exists, skipping..."
|
||||||
|
else
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"name": "Slack",
|
||||||
|
"type": "slack",
|
||||||
|
"isDefault": false,
|
||||||
|
"sendReminder": false,
|
||||||
|
"uploadImage": true
|
||||||
|
}' \
|
||||||
|
http://admin:admin@grafana.loc/api/alert-notifications
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
slack() {
|
||||||
|
enabled=true
|
||||||
|
url=''
|
||||||
|
remind=false
|
||||||
|
remindEvery='10m'
|
||||||
|
|
||||||
|
while getopts ":e:u:dr" o; do
|
||||||
|
case "${o}" in
|
||||||
|
e)
|
||||||
|
remindEvery=${OPTARG}
|
||||||
|
;;
|
||||||
|
u)
|
||||||
|
url=${OPTARG}
|
||||||
|
;;
|
||||||
|
d)
|
||||||
|
enabled=false
|
||||||
|
;;
|
||||||
|
r)
|
||||||
|
remind=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift $((OPTIND-1))
|
||||||
|
|
||||||
|
curl -X PUT \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Slack",
|
||||||
|
"type": "slack",
|
||||||
|
"isDefault": '$enabled',
|
||||||
|
"sendReminder": '$remind',
|
||||||
|
"frequency": "'$remindEvery'",
|
||||||
|
"uploadImage": true,
|
||||||
|
"settings": {
|
||||||
|
"url": "'$url'"
|
||||||
|
}
|
||||||
|
}' \
|
||||||
|
http://admin:admin@grafana.loc/api/alert-notifications/2
|
||||||
|
}
|
||||||
|
|
||||||
|
provision() {
|
||||||
|
alerts=1
|
||||||
|
condition=65
|
||||||
|
while getopts ":a:c:" o; do
|
||||||
|
case "${o}" in
|
||||||
|
a)
|
||||||
|
alerts=${OPTARG}
|
||||||
|
;;
|
||||||
|
c)
|
||||||
|
condition=${OPTARG}
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
shift $((OPTIND-1))
|
||||||
|
|
||||||
|
requiresJsonnet
|
||||||
|
|
||||||
|
rm -rf grafana/provisioning/dashboards/alerts/alert-*.json
|
||||||
|
jsonnet -m grafana/provisioning/dashboards/alerts grafana/provisioning/alerts.jsonnet --ext-code alerts=$alerts --ext-code condition=$condition
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
-d '{"paused":true}' \
|
||||||
|
http://admin:admin@grafana.loc/api/admin/pause-all-alerts
|
||||||
|
}
|
||||||
|
|
||||||
|
unpause() {
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
-d '{"paused":false}' \
|
||||||
|
http://admin:admin@grafana.loc/api/admin/pause-all-alerts
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo -e "Usage: ./alerts.sh COMMAND [OPTIONS]\n"
|
||||||
|
echo -e "Commands"
|
||||||
|
echo -e " setup\t\t creates default alert notification channels"
|
||||||
|
echo -e " slack\t\t configure slack notification channel"
|
||||||
|
echo -e " [-d]\t\t\t disable notifier, default enabled"
|
||||||
|
echo -e " [-u]\t\t\t url"
|
||||||
|
echo -e " [-r]\t\t\t send reminders"
|
||||||
|
echo -e " [-e <remind every>]\t\t default 10m\n"
|
||||||
|
echo -e " provision\t provision alerts"
|
||||||
|
echo -e " [-a <alert rule count>]\t default 1"
|
||||||
|
echo -e " [-c <condition value>]\t default 65\n"
|
||||||
|
echo -e " pause\t\t pause all alerts"
|
||||||
|
echo -e " unpause\t unpause all alerts"
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
local cmd=$1
|
||||||
|
|
||||||
|
if [[ $cmd == "setup" ]]; then
|
||||||
|
setup
|
||||||
|
elif [[ $cmd == "slack" ]]; then
|
||||||
|
slack "${@:2}"
|
||||||
|
elif [[ $cmd == "provision" ]]; then
|
||||||
|
provision "${@:2}"
|
||||||
|
elif [[ $cmd == "pause" ]]; then
|
||||||
|
pause
|
||||||
|
elif [[ $cmd == "unpause" ]]; then
|
||||||
|
unpause
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$cmd" ]]; then
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
57
devenv/docker/ha_test/docker-compose.yaml
Normal file
57
devenv/docker/ha_test/docker-compose.yaml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
version: "2.1"
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx-proxy:
|
||||||
|
image: jwilder/nginx-proxy
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||||
|
|
||||||
|
mysql:
|
||||||
|
image: mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: rootpass
|
||||||
|
MYSQL_DATABASE: grafana
|
||||||
|
MYSQL_USER: grafana
|
||||||
|
MYSQL_PASSWORD: password
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:dev
|
||||||
|
volumes:
|
||||||
|
- ./grafana/provisioning/:/etc/grafana/provisioning/
|
||||||
|
environment:
|
||||||
|
- VIRTUAL_HOST=grafana.loc
|
||||||
|
- GF_SERVER_ROOT_URL=http://grafana.loc
|
||||||
|
- GF_DATABASE_TYPE=mysql
|
||||||
|
- GF_DATABASE_HOST=mysql:3306
|
||||||
|
- GF_DATABASE_NAME=grafana
|
||||||
|
- GF_DATABASE_USER=grafana
|
||||||
|
- GF_DATABASE_PASSWORD=password
|
||||||
|
- GF_SESSION_PROVIDER=mysql
|
||||||
|
- GF_SESSION_PROVIDER_CONFIG=grafana:password@tcp(mysql:3306)/grafana?allowNativePasswords=true
|
||||||
|
ports:
|
||||||
|
- 3000
|
||||||
|
depends_on:
|
||||||
|
mysql:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:v2.4.2
|
||||||
|
volumes:
|
||||||
|
- ./prometheus/:/etc/prometheus/
|
||||||
|
environment:
|
||||||
|
- VIRTUAL_HOST=prometheus.loc
|
||||||
|
ports:
|
||||||
|
- 9090
|
||||||
|
|
||||||
|
# mysqld-exporter:
|
||||||
|
# image: prom/mysqld-exporter
|
||||||
|
# environment:
|
||||||
|
# - DATA_SOURCE_NAME=grafana:password@(mysql:3306)/
|
||||||
|
# ports:
|
||||||
|
# - 9104
|
202
devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet
Normal file
202
devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
local numAlerts = std.extVar('alerts');
|
||||||
|
local condition = std.extVar('condition');
|
||||||
|
local arr = std.range(1, numAlerts);
|
||||||
|
|
||||||
|
local alertDashboardTemplate = {
|
||||||
|
"editable": true,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"alert": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"evaluator": {
|
||||||
|
"params": [
|
||||||
|
65
|
||||||
|
],
|
||||||
|
"type": "gt"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "and"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"params": [
|
||||||
|
"A",
|
||||||
|
"5m",
|
||||||
|
"now"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reducer": {
|
||||||
|
"params": [],
|
||||||
|
"type": "avg"
|
||||||
|
},
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"executionErrorState": "alerting",
|
||||||
|
"frequency": "10s",
|
||||||
|
"handler": 1,
|
||||||
|
"name": "bulk alerting",
|
||||||
|
"noDataState": "no_data",
|
||||||
|
"notifications": [
|
||||||
|
{
|
||||||
|
"id": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"aliasColors": {},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 9,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"legend": {
|
||||||
|
"avg": false,
|
||||||
|
"current": false,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 1,
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"$$hashKey": "object:117",
|
||||||
|
"expr": "go_goroutines",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [
|
||||||
|
{
|
||||||
|
"colorMode": "critical",
|
||||||
|
"fill": true,
|
||||||
|
"line": true,
|
||||||
|
"op": "gt",
|
||||||
|
"value": 50
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Panel Title",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 16,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "New dashboard",
|
||||||
|
"uid": null,
|
||||||
|
"version": 0
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
['alert-' + std.toString(x) + '.json']:
|
||||||
|
alertDashboardTemplate + {
|
||||||
|
panels: [
|
||||||
|
alertDashboardTemplate.panels[0] +
|
||||||
|
{
|
||||||
|
alert+: {
|
||||||
|
name: 'Alert rule ' + x,
|
||||||
|
conditions: [
|
||||||
|
alertDashboardTemplate.panels[0].alert.conditions[0] +
|
||||||
|
{
|
||||||
|
evaluator+: {
|
||||||
|
params: [condition]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
uid: 'alert-' + x,
|
||||||
|
title: 'Alert ' + x
|
||||||
|
},
|
||||||
|
for x in arr
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: 'Alerts'
|
||||||
|
folder: 'Alerts'
|
||||||
|
type: file
|
||||||
|
options:
|
||||||
|
path: /etc/grafana/provisioning/dashboards/alerts
|
@ -0,0 +1,172 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": "-- Grafana --",
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"gnetId": null,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"aliasColors": {
|
||||||
|
"Active alerts": "#bf1b00"
|
||||||
|
},
|
||||||
|
"bars": false,
|
||||||
|
"dashLength": 10,
|
||||||
|
"dashes": false,
|
||||||
|
"datasource": "Prometheus",
|
||||||
|
"fill": 1,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 12,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"interval": "",
|
||||||
|
"legend": {
|
||||||
|
"alignAsTable": true,
|
||||||
|
"avg": false,
|
||||||
|
"current": true,
|
||||||
|
"max": false,
|
||||||
|
"min": false,
|
||||||
|
"rightSide": true,
|
||||||
|
"show": true,
|
||||||
|
"total": false,
|
||||||
|
"values": true
|
||||||
|
},
|
||||||
|
"lines": true,
|
||||||
|
"linewidth": 2,
|
||||||
|
"links": [],
|
||||||
|
"nullPointMode": "null",
|
||||||
|
"percentage": false,
|
||||||
|
"pointradius": 5,
|
||||||
|
"points": false,
|
||||||
|
"renderer": "flot",
|
||||||
|
"seriesOverrides": [
|
||||||
|
{
|
||||||
|
"alias": "Active grafana instances",
|
||||||
|
"dashes": true,
|
||||||
|
"fill": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"spaceLength": 10,
|
||||||
|
"stack": false,
|
||||||
|
"steppedLine": false,
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"expr": "sum(increase(grafana_alerting_notification_sent_total[1m])) by(job)",
|
||||||
|
"format": "time_series",
|
||||||
|
"instant": false,
|
||||||
|
"interval": "1m",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "Notifications sent",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "min(grafana_alerting_active_alerts) without(instance)",
|
||||||
|
"format": "time_series",
|
||||||
|
"interval": "1m",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "Active alerts",
|
||||||
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr": "count(up{job=\"grafana\"})",
|
||||||
|
"format": "time_series",
|
||||||
|
"intervalFactor": 1,
|
||||||
|
"legendFormat": "Active grafana instances",
|
||||||
|
"refId": "C"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"thresholds": [],
|
||||||
|
"timeFrom": null,
|
||||||
|
"timeShift": null,
|
||||||
|
"title": "Notifications sent vs active alerts",
|
||||||
|
"tooltip": {
|
||||||
|
"shared": true,
|
||||||
|
"sort": 0,
|
||||||
|
"value_type": "individual"
|
||||||
|
},
|
||||||
|
"type": "graph",
|
||||||
|
"xaxis": {
|
||||||
|
"buckets": null,
|
||||||
|
"mode": "time",
|
||||||
|
"name": null,
|
||||||
|
"show": true,
|
||||||
|
"values": []
|
||||||
|
},
|
||||||
|
"yaxes": [
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": "0",
|
||||||
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "short",
|
||||||
|
"label": null,
|
||||||
|
"logBase": 1,
|
||||||
|
"max": null,
|
||||||
|
"min": null,
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"yaxis": {
|
||||||
|
"align": false,
|
||||||
|
"alignLevel": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"schemaVersion": 16,
|
||||||
|
"style": "dark",
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {
|
||||||
|
"refresh_intervals": [
|
||||||
|
"5s",
|
||||||
|
"10s",
|
||||||
|
"30s",
|
||||||
|
"1m",
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"30m",
|
||||||
|
"1h",
|
||||||
|
"2h",
|
||||||
|
"1d"
|
||||||
|
],
|
||||||
|
"time_options": [
|
||||||
|
"5m",
|
||||||
|
"15m",
|
||||||
|
"1h",
|
||||||
|
"6h",
|
||||||
|
"12h",
|
||||||
|
"24h",
|
||||||
|
"2d",
|
||||||
|
"7d",
|
||||||
|
"30d"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Overview",
|
||||||
|
"uid": "xHy7-hAik",
|
||||||
|
"version": 6
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
datasources:
|
||||||
|
- name: Prometheus
|
||||||
|
type: prometheus
|
||||||
|
access: proxy
|
||||||
|
url: http://prometheus:9090
|
||||||
|
jsonData:
|
||||||
|
timeInterval: 10s
|
||||||
|
queryTimeout: 30s
|
||||||
|
httpMethod: POST
|
39
devenv/docker/ha_test/prometheus/prometheus.yml
Normal file
39
devenv/docker/ha_test/prometheus/prometheus.yml
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# my global config
|
||||||
|
global:
|
||||||
|
scrape_interval: 10s # By default, scrape targets every 15 seconds.
|
||||||
|
evaluation_interval: 10s # By default, scrape targets every 15 seconds.
|
||||||
|
# scrape_timeout is set to the global default (10s).
|
||||||
|
|
||||||
|
# Load and evaluate rules in this file every 'evaluation_interval' seconds.
|
||||||
|
#rule_files:
|
||||||
|
# - "alert.rules"
|
||||||
|
# - "first.rules"
|
||||||
|
# - "second.rules"
|
||||||
|
|
||||||
|
# alerting:
|
||||||
|
# alertmanagers:
|
||||||
|
# - scheme: http
|
||||||
|
# static_configs:
|
||||||
|
# - targets:
|
||||||
|
# - "127.0.0.1:9093"
|
||||||
|
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'prometheus'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9090']
|
||||||
|
|
||||||
|
- job_name: 'grafana'
|
||||||
|
dns_sd_configs:
|
||||||
|
- names:
|
||||||
|
- 'grafana'
|
||||||
|
type: 'A'
|
||||||
|
port: 3000
|
||||||
|
refresh_interval: 10s
|
||||||
|
|
||||||
|
# - job_name: 'mysql'
|
||||||
|
# dns_sd_configs:
|
||||||
|
# - names:
|
||||||
|
# - 'mysqld-exporter'
|
||||||
|
# type: 'A'
|
||||||
|
# port: 9104
|
||||||
|
# refresh_interval: 10s
|
@ -22,6 +22,10 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
anonString = "Anonymous"
|
||||||
|
)
|
||||||
|
|
||||||
func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) {
|
func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) {
|
||||||
if !c.IsSignedIn {
|
if !c.IsSignedIn {
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -64,7 +68,7 @@ func GetDashboard(c *m.ReqContext) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Finding creator and last updater of the dashboard
|
// Finding creator and last updater of the dashboard
|
||||||
updater, creator := "Anonymous", "Anonymous"
|
updater, creator := anonString, anonString
|
||||||
if dash.UpdatedBy > 0 {
|
if dash.UpdatedBy > 0 {
|
||||||
updater = getUserLogin(dash.UpdatedBy)
|
updater = getUserLogin(dash.UpdatedBy)
|
||||||
}
|
}
|
||||||
@ -128,7 +132,7 @@ func getUserLogin(userID int64) string {
|
|||||||
query := m.GetUserByIdQuery{Id: userID}
|
query := m.GetUserByIdQuery{Id: userID}
|
||||||
err := bus.Dispatch(&query)
|
err := bus.Dispatch(&query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "Anonymous"
|
return anonString
|
||||||
}
|
}
|
||||||
return query.Result.Login
|
return query.Result.Login
|
||||||
}
|
}
|
||||||
@ -403,7 +407,7 @@ func GetDashboardVersion(c *m.ReqContext) Response {
|
|||||||
return Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
|
return Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
creator := "Anonymous"
|
creator := anonString
|
||||||
if query.Result.CreatedBy > 0 {
|
if query.Result.CreatedBy > 0 {
|
||||||
creator = getUserLogin(query.Result.CreatedBy)
|
creator = getUserLogin(query.Result.CreatedBy)
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder {
|
|||||||
canAdmin, _ := g.CanAdmin()
|
canAdmin, _ := g.CanAdmin()
|
||||||
|
|
||||||
// Finding creator and last updater of the folder
|
// Finding creator and last updater of the folder
|
||||||
updater, creator := "Anonymous", "Anonymous"
|
updater, creator := anonString, anonString
|
||||||
if folder.CreatedBy > 0 {
|
if folder.CreatedBy > 0 {
|
||||||
creator = getUserLogin(folder.CreatedBy)
|
creator = getUserLogin(folder.CreatedBy)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,12 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Themes
|
||||||
|
lightName = "light"
|
||||||
|
darkName = "dark"
|
||||||
|
)
|
||||||
|
|
||||||
func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||||
settings, err := getFrontendSettingsMap(c)
|
settings, err := getFrontendSettingsMap(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -60,7 +66,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
|||||||
OrgRole: c.OrgRole,
|
OrgRole: c.OrgRole,
|
||||||
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
||||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||||
LightTheme: prefs.Theme == "light",
|
LightTheme: prefs.Theme == lightName,
|
||||||
Timezone: prefs.Timezone,
|
Timezone: prefs.Timezone,
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
HelpFlags1: c.HelpFlags1,
|
HelpFlags1: c.HelpFlags1,
|
||||||
@ -88,12 +94,12 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
themeURLParam := c.Query("theme")
|
themeURLParam := c.Query("theme")
|
||||||
if themeURLParam == "light" {
|
if themeURLParam == lightName {
|
||||||
data.User.LightTheme = true
|
data.User.LightTheme = true
|
||||||
data.Theme = "light"
|
data.Theme = lightName
|
||||||
} else if themeURLParam == "dark" {
|
} else if themeURLParam == darkName {
|
||||||
data.User.LightTheme = false
|
data.User.LightTheme = false
|
||||||
data.Theme = "dark"
|
data.Theme = darkName
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasEditPermissionInFoldersQuery.Result {
|
if hasEditPermissionInFoldersQuery.Result {
|
||||||
|
@ -2,12 +2,15 @@ package imguploader
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||||
"github.com/aws/aws-sdk-go/aws/endpoints"
|
"github.com/aws/aws-sdk-go/aws/endpoints"
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
@ -50,7 +53,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
|
|||||||
SecretAccessKey: u.secretKey,
|
SecretAccessKey: u.secretKey,
|
||||||
}},
|
}},
|
||||||
&credentials.EnvProvider{},
|
&credentials.EnvProvider{},
|
||||||
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
|
remoteCredProvider(sess),
|
||||||
})
|
})
|
||||||
cfg := &aws.Config{
|
cfg := &aws.Config{
|
||||||
Region: aws.String(u.region),
|
Region: aws.String(u.region),
|
||||||
@ -85,3 +88,27 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
|
|||||||
}
|
}
|
||||||
return image_url, nil
|
return image_url, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func remoteCredProvider(sess *session.Session) credentials.Provider {
|
||||||
|
ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
|
||||||
|
|
||||||
|
if len(ecsCredURI) > 0 {
|
||||||
|
return ecsCredProvider(sess, ecsCredURI)
|
||||||
|
}
|
||||||
|
return ec2RoleProvider(sess)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ecsCredProvider(sess *session.Session, uri string) credentials.Provider {
|
||||||
|
const host = `169.254.170.2`
|
||||||
|
|
||||||
|
d := defaults.Get()
|
||||||
|
return endpointcreds.NewProviderClient(
|
||||||
|
*d.Config,
|
||||||
|
d.Handlers,
|
||||||
|
fmt.Sprintf("http://%s%s", host, uri),
|
||||||
|
func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute })
|
||||||
|
}
|
||||||
|
|
||||||
|
func ec2RoleProvider(sess *session.Session) credentials.Provider {
|
||||||
|
return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}
|
||||||
|
}
|
||||||
|
@ -8,6 +8,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nullString = "null"
|
||||||
|
)
|
||||||
|
|
||||||
// Float is a nullable float64.
|
// Float is a nullable float64.
|
||||||
// It does not consider zero values to be null.
|
// It does not consider zero values to be null.
|
||||||
// It will decode to null, not zero, if null.
|
// It will decode to null, not zero, if null.
|
||||||
@ -68,7 +72,7 @@ func (f *Float) UnmarshalJSON(data []byte) error {
|
|||||||
// It will return an error if the input is not an integer, blank, or "null".
|
// It will return an error if the input is not an integer, blank, or "null".
|
||||||
func (f *Float) UnmarshalText(text []byte) error {
|
func (f *Float) UnmarshalText(text []byte) error {
|
||||||
str := string(text)
|
str := string(text)
|
||||||
if str == "" || str == "null" {
|
if str == "" || str == nullString {
|
||||||
f.Valid = false
|
f.Valid = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -82,7 +86,7 @@ func (f *Float) UnmarshalText(text []byte) error {
|
|||||||
// It will encode null if this Float is null.
|
// It will encode null if this Float is null.
|
||||||
func (f Float) MarshalJSON() ([]byte, error) {
|
func (f Float) MarshalJSON() ([]byte, error) {
|
||||||
if !f.Valid {
|
if !f.Valid {
|
||||||
return []byte("null"), nil
|
return []byte(nullString), nil
|
||||||
}
|
}
|
||||||
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
|
return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil
|
||||||
}
|
}
|
||||||
@ -100,7 +104,7 @@ func (f Float) MarshalText() ([]byte, error) {
|
|||||||
// It will encode a blank string if this Float is null.
|
// It will encode a blank string if this Float is null.
|
||||||
func (f Float) String() string {
|
func (f Float) String() string {
|
||||||
if !f.Valid {
|
if !f.Valid {
|
||||||
return "null"
|
return nullString
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%1.3f", f.Float64)
|
return fmt.Sprintf("%1.3f", f.Float64)
|
||||||
@ -109,7 +113,7 @@ func (f Float) String() string {
|
|||||||
// FullString returns float as string in full precision
|
// FullString returns float as string in full precision
|
||||||
func (f Float) FullString() string {
|
func (f Float) FullString() string {
|
||||||
if !f.Valid {
|
if !f.Valid {
|
||||||
return "null"
|
return nullString
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%f", f.Float64)
|
return fmt.Sprintf("%f", f.Float64)
|
||||||
|
@ -10,6 +10,10 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
triggMetrString = "Triggered metrics:\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
type NotifierBase struct {
|
type NotifierBase struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
|
@ -61,7 +61,7 @@ func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
|
|||||||
|
|
||||||
state := evalContext.Rule.State
|
state := evalContext.Rule.State
|
||||||
|
|
||||||
customData := "Triggered metrics:\n\n"
|
customData := triggMetrString
|
||||||
for _, evt := range evalContext.EvalMatches {
|
for _, evt := range evalContext.EvalMatches {
|
||||||
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
customData := "Triggered metrics:\n\n"
|
customData := triggMetrString
|
||||||
for _, evt := range evalContext.EvalMatches {
|
for _, evt := range evalContext.EvalMatches {
|
||||||
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
|
|||||||
if evalContext.Rule.State == m.AlertStateOK {
|
if evalContext.Rule.State == m.AlertStateOK {
|
||||||
eventType = "resolve"
|
eventType = "resolve"
|
||||||
}
|
}
|
||||||
customData := "Triggered metrics:\n\n"
|
customData := triggMetrString
|
||||||
for _, evt := range evalContext.EvalMatches {
|
for _, evt := range evalContext.EvalMatches {
|
||||||
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
|
||||||
}
|
}
|
||||||
|
@ -43,26 +43,6 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
|
|||||||
log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.")
|
log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
||||||
log.Error("Cannot read directory", "error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
copy := path
|
|
||||||
path, err := filepath.Abs(path)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Could not create absolute path ", "path", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
path, err = filepath.EvalSymlinks(path)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Failed to read content of symlinked path: %s", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
path = copy
|
|
||||||
log.Info("falling back to original path due to EvalSymlink/Abs failure")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &fileReader{
|
return &fileReader{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
Path: path,
|
Path: path,
|
||||||
@ -99,7 +79,8 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fr *fileReader) startWalkingDisk() error {
|
func (fr *fileReader) startWalkingDisk() error {
|
||||||
if _, err := os.Stat(fr.Path); err != nil {
|
resolvedPath := fr.resolvePath(fr.Path)
|
||||||
|
if _, err := os.Stat(resolvedPath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -116,7 +97,7 @@ func (fr *fileReader) startWalkingDisk() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filesFoundOnDisk := map[string]os.FileInfo{}
|
filesFoundOnDisk := map[string]os.FileInfo{}
|
||||||
err = filepath.Walk(fr.Path, createWalkFn(filesFoundOnDisk))
|
err = filepath.Walk(resolvedPath, createWalkFn(filesFoundOnDisk))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -344,6 +325,29 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fr *fileReader) resolvePath(path string) string {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
fr.log.Error("Cannot read directory", "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy := path
|
||||||
|
path, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
fr.log.Error("Could not create absolute path ", "path", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err = filepath.EvalSymlinks(path)
|
||||||
|
if err != nil {
|
||||||
|
fr.log.Error("Failed to read content of symlinked path: %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
path = copy
|
||||||
|
fr.log.Info("falling back to original path due to EvalSymlink/Abs failure")
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
type provisioningMetadata struct {
|
type provisioningMetadata struct {
|
||||||
uid string
|
uid string
|
||||||
title string
|
title string
|
||||||
|
@ -30,10 +30,11 @@ func TestProvsionedSymlinkedFolder(t *testing.T) {
|
|||||||
want, err := filepath.Abs(containingId)
|
want, err := filepath.Abs(containingId)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected err to be nill")
|
t.Errorf("expected err to be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
if reader.Path != want {
|
resolvedPath := reader.resolvePath(reader.Path)
|
||||||
t.Errorf("got %s want %s", reader.Path, want)
|
if resolvedPath != want {
|
||||||
|
t.Errorf("got %s want %s", resolvedPath, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,8 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
|||||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(filepath.IsAbs(reader.Path), ShouldBeTrue)
|
resolvedPath := reader.resolvePath(reader.Path)
|
||||||
|
So(filepath.IsAbs(resolvedPath), ShouldBeTrue)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -46,10 +46,14 @@ func (e *Error) Error() string {
|
|||||||
return e.s
|
return e.s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
grafanaCom = "grafana_com"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
SocialBaseUrl = "/login/"
|
SocialBaseUrl = "/login/"
|
||||||
SocialMap = make(map[string]SocialConnector)
|
SocialMap = make(map[string]SocialConnector)
|
||||||
allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"}
|
allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", grafanaCom}
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewOAuthService() {
|
func NewOAuthService() {
|
||||||
@ -82,7 +86,7 @@ func NewOAuthService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if name == "grafananet" {
|
if name == "grafananet" {
|
||||||
name = "grafana_com"
|
name = grafanaCom
|
||||||
}
|
}
|
||||||
|
|
||||||
setting.OAuthService.OAuthInfos[name] = info
|
setting.OAuthService.OAuthInfos[name] = info
|
||||||
@ -159,7 +163,7 @@ func NewOAuthService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if name == "grafana_com" {
|
if name == grafanaCom {
|
||||||
config = oauth2.Config{
|
config = oauth2.Config{
|
||||||
ClientID: info.ClientId,
|
ClientID: info.ClientId,
|
||||||
ClientSecret: info.ClientSecret,
|
ClientSecret: info.ClientSecret,
|
||||||
@ -171,7 +175,7 @@ func NewOAuthService() {
|
|||||||
Scopes: info.Scopes,
|
Scopes: info.Scopes,
|
||||||
}
|
}
|
||||||
|
|
||||||
SocialMap["grafana_com"] = &SocialGrafanaCom{
|
SocialMap[grafanaCom] = &SocialGrafanaCom{
|
||||||
SocialBase: &SocialBase{
|
SocialBase: &SocialBase{
|
||||||
Config: &config,
|
Config: &config,
|
||||||
log: logger,
|
log: logger,
|
||||||
@ -194,7 +198,7 @@ var GetOAuthProviders = func(cfg *setting.Cfg) map[string]bool {
|
|||||||
|
|
||||||
for _, name := range allOauthes {
|
for _, name := range allOauthes {
|
||||||
if name == "grafananet" {
|
if name == "grafananet" {
|
||||||
name = "grafana_com"
|
name = grafanaCom
|
||||||
}
|
}
|
||||||
|
|
||||||
sec := cfg.Raw.Section("auth." + name)
|
sec := cfg.Raw.Section("auth." + name)
|
||||||
|
@ -235,7 +235,7 @@ func parseMultiSelectValue(input string) []string {
|
|||||||
func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||||
regions := []string{
|
regions := []string{
|
||||||
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1",
|
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1",
|
||||||
"eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
|
"eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", "us-isob-east-1", "us-iso-east-1",
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]suggestData, 0)
|
result := make([]suggestData, 0)
|
||||||
|
@ -13,6 +13,19 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
"github.com/grafana/grafana/pkg/tsdb/elasticsearch/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Metric types
|
||||||
|
countType = "count"
|
||||||
|
percentilesType = "percentiles"
|
||||||
|
extendedStatsType = "extended_stats"
|
||||||
|
// Bucket types
|
||||||
|
dateHistType = "date_histogram"
|
||||||
|
histogramType = "histogram"
|
||||||
|
filtersType = "filters"
|
||||||
|
termsType = "terms"
|
||||||
|
geohashGridType = "geohash_grid"
|
||||||
|
)
|
||||||
|
|
||||||
type responseParser struct {
|
type responseParser struct {
|
||||||
Responses []*es.SearchResponse
|
Responses []*es.SearchResponse
|
||||||
Targets []*Query
|
Targets []*Query
|
||||||
@ -81,7 +94,7 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if depth == maxDepth {
|
if depth == maxDepth {
|
||||||
if aggDef.Type == "date_histogram" {
|
if aggDef.Type == dateHistType {
|
||||||
err = rp.processMetrics(esAgg, target, series, props)
|
err = rp.processMetrics(esAgg, target, series, props)
|
||||||
} else {
|
} else {
|
||||||
err = rp.processAggregationDocs(esAgg, aggDef, target, table, props)
|
err = rp.processAggregationDocs(esAgg, aggDef, target, table, props)
|
||||||
@ -149,7 +162,7 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query,
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch metric.Type {
|
switch metric.Type {
|
||||||
case "count":
|
case countType:
|
||||||
newSeries := tsdb.TimeSeries{
|
newSeries := tsdb.TimeSeries{
|
||||||
Tags: make(map[string]string),
|
Tags: make(map[string]string),
|
||||||
}
|
}
|
||||||
@ -164,10 +177,10 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query,
|
|||||||
for k, v := range props {
|
for k, v := range props {
|
||||||
newSeries.Tags[k] = v
|
newSeries.Tags[k] = v
|
||||||
}
|
}
|
||||||
newSeries.Tags["metric"] = "count"
|
newSeries.Tags["metric"] = countType
|
||||||
*series = append(*series, &newSeries)
|
*series = append(*series, &newSeries)
|
||||||
|
|
||||||
case "percentiles":
|
case percentilesType:
|
||||||
buckets := esAgg.Get("buckets").MustArray()
|
buckets := esAgg.Get("buckets").MustArray()
|
||||||
if len(buckets) == 0 {
|
if len(buckets) == 0 {
|
||||||
break
|
break
|
||||||
@ -198,7 +211,7 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query,
|
|||||||
}
|
}
|
||||||
*series = append(*series, &newSeries)
|
*series = append(*series, &newSeries)
|
||||||
}
|
}
|
||||||
case "extended_stats":
|
case extendedStatsType:
|
||||||
buckets := esAgg.Get("buckets").MustArray()
|
buckets := esAgg.Get("buckets").MustArray()
|
||||||
|
|
||||||
metaKeys := make([]string, 0)
|
metaKeys := make([]string, 0)
|
||||||
@ -312,9 +325,9 @@ func (rp *responseParser) processAggregationDocs(esAgg *simplejson.Json, aggDef
|
|||||||
|
|
||||||
for _, metric := range target.Metrics {
|
for _, metric := range target.Metrics {
|
||||||
switch metric.Type {
|
switch metric.Type {
|
||||||
case "count":
|
case countType:
|
||||||
addMetricValue(&values, rp.getMetricName(metric.Type), castToNullFloat(bucket.Get("doc_count")))
|
addMetricValue(&values, rp.getMetricName(metric.Type), castToNullFloat(bucket.Get("doc_count")))
|
||||||
case "extended_stats":
|
case extendedStatsType:
|
||||||
metaKeys := make([]string, 0)
|
metaKeys := make([]string, 0)
|
||||||
meta := metric.Meta.MustMap()
|
meta := metric.Meta.MustMap()
|
||||||
for k := range meta {
|
for k := range meta {
|
||||||
@ -366,7 +379,7 @@ func (rp *responseParser) processAggregationDocs(esAgg *simplejson.Json, aggDef
|
|||||||
func (rp *responseParser) trimDatapoints(series *tsdb.TimeSeriesSlice, target *Query) {
|
func (rp *responseParser) trimDatapoints(series *tsdb.TimeSeriesSlice, target *Query) {
|
||||||
var histogram *BucketAgg
|
var histogram *BucketAgg
|
||||||
for _, bucketAgg := range target.BucketAggs {
|
for _, bucketAgg := range target.BucketAggs {
|
||||||
if bucketAgg.Type == "date_histogram" {
|
if bucketAgg.Type == dateHistType {
|
||||||
histogram = bucketAgg
|
histogram = bucketAgg
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -75,15 +75,15 @@ func (e *timeSeriesQuery) execute() (*tsdb.Response, error) {
|
|||||||
// iterate backwards to create aggregations bottom-down
|
// iterate backwards to create aggregations bottom-down
|
||||||
for _, bucketAgg := range q.BucketAggs {
|
for _, bucketAgg := range q.BucketAggs {
|
||||||
switch bucketAgg.Type {
|
switch bucketAgg.Type {
|
||||||
case "date_histogram":
|
case dateHistType:
|
||||||
aggBuilder = addDateHistogramAgg(aggBuilder, bucketAgg, from, to)
|
aggBuilder = addDateHistogramAgg(aggBuilder, bucketAgg, from, to)
|
||||||
case "histogram":
|
case histogramType:
|
||||||
aggBuilder = addHistogramAgg(aggBuilder, bucketAgg)
|
aggBuilder = addHistogramAgg(aggBuilder, bucketAgg)
|
||||||
case "filters":
|
case filtersType:
|
||||||
aggBuilder = addFiltersAgg(aggBuilder, bucketAgg)
|
aggBuilder = addFiltersAgg(aggBuilder, bucketAgg)
|
||||||
case "terms":
|
case termsType:
|
||||||
aggBuilder = addTermsAgg(aggBuilder, bucketAgg, q.Metrics)
|
aggBuilder = addTermsAgg(aggBuilder, bucketAgg, q.Metrics)
|
||||||
case "geohash_grid":
|
case geohashGridType:
|
||||||
aggBuilder = addGeoHashGridAgg(aggBuilder, bucketAgg)
|
aggBuilder = addGeoHashGridAgg(aggBuilder, bucketAgg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
public/app/core/components/LayoutSelector/LayoutSelector.tsx
Normal file
39
public/app/core/components/LayoutSelector/LayoutSelector.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
|
||||||
|
export type LayoutMode = LayoutModes.Grid | LayoutModes.List;
|
||||||
|
|
||||||
|
export enum LayoutModes {
|
||||||
|
Grid = 'grid',
|
||||||
|
List = 'list',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mode: LayoutMode;
|
||||||
|
onLayoutModeChanged: (mode: LayoutMode) => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const LayoutSelector: SFC<Props> = props => {
|
||||||
|
const { mode, onLayoutModeChanged } = props;
|
||||||
|
return (
|
||||||
|
<div className="layout-selector">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onLayoutModeChanged(LayoutModes.List);
|
||||||
|
}}
|
||||||
|
className={mode === LayoutModes.List ? 'active' : ''}
|
||||||
|
>
|
||||||
|
<i className="fa fa-list" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onLayoutModeChanged(LayoutModes.Grid);
|
||||||
|
}}
|
||||||
|
className={mode === LayoutModes.Grid ? 'active' : ''}
|
||||||
|
>
|
||||||
|
<i className="fa fa-th" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LayoutSelector;
|
@ -29,6 +29,7 @@ describe('timeSrv', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() });
|
timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() });
|
||||||
timeSrv.init(_dashboard);
|
timeSrv.init(_dashboard);
|
||||||
|
_dashboard.refresh = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('timeRange', () => {
|
describe('timeRange', () => {
|
||||||
@ -79,6 +80,23 @@ describe('timeSrv', () => {
|
|||||||
expect(time.to.valueOf()).toEqual(new Date('2014-05-20T03:10:22Z').getTime());
|
expect(time.to.valueOf()).toEqual(new Date('2014-05-20T03:10:22Z').getTime());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ignore refresh if time absolute', () => {
|
||||||
|
location = {
|
||||||
|
search: jest.fn(() => ({
|
||||||
|
from: '20140410T052010',
|
||||||
|
to: '20140520T031022',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
timeSrv = new TimeSrv(rootScope, jest.fn(), location, timer, { isGrafanaVisibile: jest.fn() });
|
||||||
|
|
||||||
|
// dashboard saved with refresh on
|
||||||
|
_dashboard.refresh = true;
|
||||||
|
timeSrv.init(_dashboard);
|
||||||
|
|
||||||
|
expect(timeSrv.refresh).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle formatted dates without time', () => {
|
it('should handle formatted dates without time', () => {
|
||||||
location = {
|
location = {
|
||||||
search: jest.fn(() => ({
|
search: jest.fn(() => ({
|
||||||
|
@ -4,7 +4,8 @@
|
|||||||
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
|
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
|
||||||
{{variable.label || variable.name}}
|
{{variable.label || variable.name}}
|
||||||
</label>
|
</label>
|
||||||
<value-select-dropdown ng-if="variable.type !== 'adhoc'" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
|
<value-select-dropdown ng-if="variable.type !== 'adhoc' && variable.type !== 'textbox'" variable="variable" on-updated="ctrl.variableUpdated(variable)"></value-select-dropdown>
|
||||||
|
<input type="text" ng-if="variable.type === 'textbox'" ng-model="variable.query" class="gf-form-input width-12" ng-blur="variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ng-keydown="$event.keyCode === 13 && variable.current.value != variable.query && variable.updateOptions() && ctrl.variableUpdated(variable);" ></input>
|
||||||
</div>
|
</div>
|
||||||
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable"></ad-hoc-filters>
|
<ad-hoc-filters ng-if="variable.type === 'adhoc'" variable="variable"></ad-hoc-filters>
|
||||||
</div>
|
</div>
|
||||||
|
@ -85,6 +85,12 @@ export class TimeSrv {
|
|||||||
if (params.to) {
|
if (params.to) {
|
||||||
this.time.to = this.parseUrlParam(params.to) || this.time.to;
|
this.time.to = this.parseUrlParam(params.to) || this.time.to;
|
||||||
}
|
}
|
||||||
|
// if absolute ignore refresh option saved to dashboard
|
||||||
|
if (params.to && params.to.indexOf('now') === -1) {
|
||||||
|
this.refresh = false;
|
||||||
|
this.dashboard.refresh = false;
|
||||||
|
}
|
||||||
|
// but if refresh explicitly set then use that
|
||||||
if (params.refresh) {
|
if (params.refresh) {
|
||||||
this.refresh = params.refresh || this.refresh;
|
this.refresh = params.refresh || this.refresh;
|
||||||
}
|
}
|
||||||
|
@ -528,10 +528,11 @@ export class Explore extends React.Component<any, ExploreState> {
|
|||||||
{!datasourceMissing ? (
|
{!datasourceMissing ? (
|
||||||
<div className="navbar-buttons">
|
<div className="navbar-buttons">
|
||||||
<Select
|
<Select
|
||||||
className="datasource-picker"
|
|
||||||
clearable={false}
|
clearable={false}
|
||||||
|
className="gf-form-input gf-form-input--form-dropdown datasource-picker"
|
||||||
onChange={this.onChangeDatasource}
|
onChange={this.onChangeDatasource}
|
||||||
options={datasources}
|
options={datasources}
|
||||||
|
isOpen={true}
|
||||||
placeholder="Loading datasources..."
|
placeholder="Loading datasources..."
|
||||||
value={selectedDatasource}
|
value={selectedDatasource}
|
||||||
/>
|
/>
|
||||||
@ -586,17 +587,17 @@ export class Explore extends React.Component<any, ExploreState> {
|
|||||||
/>
|
/>
|
||||||
<div className="result-options">
|
<div className="result-options">
|
||||||
{supportsGraph ? (
|
{supportsGraph ? (
|
||||||
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
||||||
Graph
|
Graph
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{supportsTable ? (
|
{supportsTable ? (
|
||||||
<button className={`btn navbar-button ${tableButtonActive}`} onClick={this.onClickTableButton}>
|
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
|
||||||
Table
|
Table
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{supportsLogs ? (
|
{supportsLogs ? (
|
||||||
<button className={`btn navbar-button ${logsButtonActive}`} onClick={this.onClickLogsButton}>
|
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
|
||||||
Logs
|
Logs
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -2,16 +2,25 @@
|
|||||||
|
|
||||||
<div class="page-container page-body">
|
<div class="page-container page-body">
|
||||||
<div class="page-action-bar">
|
<div class="page-action-bar">
|
||||||
<label class="gf-form gf-form--grow gf-form--has-input-icon">
|
<label class="gf-form gf-form--has-input-icon">
|
||||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" placeholder="Filter by username or email" />
|
<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" placeholder="Filter by username or email" />
|
||||||
<i class="gf-form-input-icon fa fa-search"></i>
|
<i class="gf-form-input-icon fa fa-search"></i>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="page-action-bar__spacer"></div>
|
<div ng-if="ctrl.pendingInvites.length" style="margin-left: 1rem">
|
||||||
|
<button class="btn toggle-btn active" ng-if="!ctrl.showInvites">
|
||||||
|
Users
|
||||||
|
</button><button class="btn toggle-btn" ng-if="!ctrl.showInvites" ng-click="ctrl.showInvites = true">
|
||||||
|
Pending Invites ({{ctrl.pendingInvites.length}})
|
||||||
|
</button>
|
||||||
|
<button class="btn toggle-btn" ng-if="ctrl.showInvites" ng-click="ctrl.showInvites = false">
|
||||||
|
Users
|
||||||
|
</button><button class="btn toggle-btn active" ng-if="ctrl.showInvites">
|
||||||
|
Pending Invites ({{ctrl.pendingInvites.length}})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-inverse" ng-show="ctrl.pendingInvites.length" ng-click="ctrl.showInvites = true">
|
<div class="page-action-bar__spacer"></div>
|
||||||
Pending Invites ({{ctrl.pendingInvites.length}})
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a class="btn btn-success" href="org/users/invite" ng-show="ctrl.canInvite">
|
<a class="btn btn-success" href="org/users/invite" ng-show="ctrl.canInvite">
|
||||||
<i class="fa fa-plus"></i>
|
<i class="fa fa-plus"></i>
|
||||||
|
31
public/app/features/plugins/PluginActionBar.test.tsx
Normal file
31
public/app/features/plugins/PluginActionBar.test.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { PluginActionBar, Props } from './PluginActionBar';
|
||||||
|
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
searchQuery: '',
|
||||||
|
layoutMode: LayoutModes.Grid,
|
||||||
|
setLayoutMode: jest.fn(),
|
||||||
|
setPluginsSearchQuery: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<PluginActionBar {...props} />);
|
||||||
|
const instance = wrapper.instance() as PluginActionBar;
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const { wrapper } = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
62
public/app/features/plugins/PluginActionBar.tsx
Normal file
62
public/app/features/plugins/PluginActionBar.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import LayoutSelector, { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||||
|
import { setLayoutMode, setPluginsSearchQuery } from './state/actions';
|
||||||
|
import { getPluginsSearchQuery, getLayoutMode } from './state/selectors';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
searchQuery: string;
|
||||||
|
layoutMode: LayoutMode;
|
||||||
|
setLayoutMode: typeof setLayoutMode;
|
||||||
|
setPluginsSearchQuery: typeof setPluginsSearchQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginActionBar extends PureComponent<Props> {
|
||||||
|
onSearchQueryChange = event => {
|
||||||
|
this.props.setPluginsSearchQuery(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { searchQuery, layoutMode, setLayoutMode } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="page-action-bar">
|
||||||
|
<div className="gf-form gf-form--grow">
|
||||||
|
<label className="gf-form--has-input-icon">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="gf-form-input width-20"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={this.onSearchQueryChange}
|
||||||
|
placeholder="Filter by name or type"
|
||||||
|
/>
|
||||||
|
<i className="gf-form-input-icon fa fa-search" />
|
||||||
|
</label>
|
||||||
|
<LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => setLayoutMode(mode)} />
|
||||||
|
</div>
|
||||||
|
<div className="page-action-bar__spacer" />
|
||||||
|
<a
|
||||||
|
className="btn btn-success"
|
||||||
|
href="https://grafana.com/plugins?utm_source=grafana_plugin_list"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Find more plugins on Grafana.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
searchQuery: getPluginsSearchQuery(state.plugins),
|
||||||
|
layoutMode: getLayoutMode(state.plugins),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setPluginsSearchQuery,
|
||||||
|
setLayoutMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PluginActionBar);
|
25
public/app/features/plugins/PluginList.test.tsx
Normal file
25
public/app/features/plugins/PluginList.test.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import PluginList from './PluginList';
|
||||||
|
import { getMockPlugins } from './__mocks__/pluginMocks';
|
||||||
|
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
plugins: getMockPlugins(5),
|
||||||
|
layoutMode: LayoutModes.Grid,
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
return shallow(<PluginList {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
32
public/app/features/plugins/PluginList.tsx
Normal file
32
public/app/features/plugins/PluginList.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
import classNames from 'classnames/bind';
|
||||||
|
import PluginListItem from './PluginListItem';
|
||||||
|
import { Plugin } from 'app/types';
|
||||||
|
import { LayoutMode, LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
plugins: Plugin[];
|
||||||
|
layoutMode: LayoutMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginList: SFC<Props> = props => {
|
||||||
|
const { plugins, layoutMode } = props;
|
||||||
|
|
||||||
|
const listStyle = classNames({
|
||||||
|
'card-section': true,
|
||||||
|
'card-list-layout-grid': layoutMode === LayoutModes.Grid,
|
||||||
|
'card-list-layout-list': layoutMode === LayoutModes.List,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={listStyle}>
|
||||||
|
<ol className="card-list">
|
||||||
|
{plugins.map((plugin, index) => {
|
||||||
|
return <PluginListItem plugin={plugin} key={`${plugin.name}-${index}`} />;
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginList;
|
33
public/app/features/plugins/PluginListItem.test.tsx
Normal file
33
public/app/features/plugins/PluginListItem.test.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import PluginListItem from './PluginListItem';
|
||||||
|
import { getMockPlugin } from './__mocks__/pluginMocks';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props = Object.assign(
|
||||||
|
{
|
||||||
|
plugin: getMockPlugin(),
|
||||||
|
},
|
||||||
|
propOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
return shallow(<PluginListItem {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render has plugin section', () => {
|
||||||
|
const mockPlugin = getMockPlugin();
|
||||||
|
mockPlugin.hasUpdate = true;
|
||||||
|
const wrapper = setup({
|
||||||
|
plugin: mockPlugin,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
39
public/app/features/plugins/PluginListItem.tsx
Normal file
39
public/app/features/plugins/PluginListItem.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
import { Plugin } from 'app/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
plugin: Plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginListItem: SFC<Props> = props => {
|
||||||
|
const { plugin } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="card-item-wrapper">
|
||||||
|
<a className="card-item" href={`plugins/${plugin.id}/edit`}>
|
||||||
|
<div className="card-item-header">
|
||||||
|
<div className="card-item-type">
|
||||||
|
<i className={`icon-gf icon-gf-${plugin.type}`} />
|
||||||
|
{plugin.type}
|
||||||
|
</div>
|
||||||
|
{plugin.hasUpdate && (
|
||||||
|
<div className="card-item-notice">
|
||||||
|
<span bs-tooltip="plugin.latestVersion">Update available!</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="card-item-body">
|
||||||
|
<figure className="card-item-figure">
|
||||||
|
<img src={plugin.info.logos.small} />
|
||||||
|
</figure>
|
||||||
|
<div className="card-item-details">
|
||||||
|
<div className="card-item-name">{plugin.name}</div>
|
||||||
|
<div className="card-item-sub-name">{`By ${plugin.info.author.name}`}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginListItem;
|
32
public/app/features/plugins/PluginListPage.test.tsx
Normal file
32
public/app/features/plugins/PluginListPage.test.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { PluginListPage, Props } from './PluginListPage';
|
||||||
|
import { NavModel, Plugin } from '../../types';
|
||||||
|
import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
navModel: {} as NavModel,
|
||||||
|
plugins: [] as Plugin[],
|
||||||
|
layoutMode: LayoutModes.Grid,
|
||||||
|
loadPlugins: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
const wrapper = shallow(<PluginListPage {...props} />);
|
||||||
|
const instance = wrapper.instance() as PluginListPage;
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper,
|
||||||
|
instance,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const { wrapper } = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
56
public/app/features/plugins/PluginListPage.tsx
Normal file
56
public/app/features/plugins/PluginListPage.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PageHeader from '../../core/components/PageHeader/PageHeader';
|
||||||
|
import PluginActionBar from './PluginActionBar';
|
||||||
|
import PluginList from './PluginList';
|
||||||
|
import { NavModel, Plugin } from '../../types';
|
||||||
|
import { loadPlugins } from './state/actions';
|
||||||
|
import { getNavModel } from '../../core/selectors/navModel';
|
||||||
|
import { getLayoutMode, getPlugins } from './state/selectors';
|
||||||
|
import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
navModel: NavModel;
|
||||||
|
plugins: Plugin[];
|
||||||
|
layoutMode: LayoutMode;
|
||||||
|
loadPlugins: typeof loadPlugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginListPage extends PureComponent<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.fetchPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchPlugins() {
|
||||||
|
await this.props.loadPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { navModel, plugins, layoutMode } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader model={navModel} />
|
||||||
|
<div className="page-container page-body">
|
||||||
|
<PluginActionBar />
|
||||||
|
{plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
navModel: getNavModel(state.navIndex, 'plugins'),
|
||||||
|
plugins: getPlugins(state.plugins),
|
||||||
|
layoutMode: getLayoutMode(state.plugins),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
loadPlugins,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(PluginListPage));
|
59
public/app/features/plugins/__mocks__/pluginMocks.ts
Normal file
59
public/app/features/plugins/__mocks__/pluginMocks.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { Plugin } from 'app/types';
|
||||||
|
|
||||||
|
export const getMockPlugins = (amount: number): Plugin[] => {
|
||||||
|
const plugins = [];
|
||||||
|
|
||||||
|
for (let i = 0; i <= amount; i++) {
|
||||||
|
plugins.push({
|
||||||
|
defaultNavUrl: 'some/url',
|
||||||
|
enabled: false,
|
||||||
|
hasUpdate: false,
|
||||||
|
id: `${i}`,
|
||||||
|
info: {
|
||||||
|
author: {
|
||||||
|
name: 'Grafana Labs',
|
||||||
|
url: 'url/to/GrafanaLabs',
|
||||||
|
},
|
||||||
|
description: 'pretty decent plugin',
|
||||||
|
links: ['one link'],
|
||||||
|
logos: { small: 'small/logo', large: 'large/logo' },
|
||||||
|
screenshots: `screenshot/${i}`,
|
||||||
|
updated: '2018-09-26',
|
||||||
|
version: '1',
|
||||||
|
},
|
||||||
|
latestVersion: `1.${i}`,
|
||||||
|
name: `pretty cool plugin-${i}`,
|
||||||
|
pinned: false,
|
||||||
|
state: '',
|
||||||
|
type: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMockPlugin = () => {
|
||||||
|
return {
|
||||||
|
defaultNavUrl: 'some/url',
|
||||||
|
enabled: false,
|
||||||
|
hasUpdate: false,
|
||||||
|
id: '1',
|
||||||
|
info: {
|
||||||
|
author: {
|
||||||
|
name: 'Grafana Labs',
|
||||||
|
url: 'url/to/GrafanaLabs',
|
||||||
|
},
|
||||||
|
description: 'pretty decent plugin',
|
||||||
|
links: ['one link'],
|
||||||
|
logos: { small: 'small/logo', large: 'large/logo' },
|
||||||
|
screenshots: 'screenshot/1',
|
||||||
|
updated: '2018-09-26',
|
||||||
|
version: '1',
|
||||||
|
},
|
||||||
|
latestVersion: '1',
|
||||||
|
name: 'pretty cool plugin 1',
|
||||||
|
pinned: false,
|
||||||
|
state: '',
|
||||||
|
type: '',
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div
|
||||||
|
className="page-action-bar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="gf-form gf-form--grow"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className="gf-form--has-input-icon"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
className="gf-form-input width-20"
|
||||||
|
onChange={[Function]}
|
||||||
|
placeholder="Filter by name or type"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<i
|
||||||
|
className="gf-form-input-icon fa fa-search"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<LayoutSelector
|
||||||
|
mode="grid"
|
||||||
|
onLayoutModeChanged={[Function]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="page-action-bar__spacer"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
className="btn btn-success"
|
||||||
|
href="https://grafana.com/plugins?utm_source=grafana_plugin_list"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Find more plugins on Grafana.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -0,0 +1,210 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<section
|
||||||
|
className="card-section card-list-layout-grid"
|
||||||
|
>
|
||||||
|
<ol
|
||||||
|
className="card-list"
|
||||||
|
>
|
||||||
|
<PluginListItem
|
||||||
|
key="pretty cool plugin-0-0"
|
||||||
|
plugin={
|
||||||
|
Object {
|
||||||
|
"defaultNavUrl": "some/url",
|
||||||
|
"enabled": false,
|
||||||
|
"hasUpdate": false,
|
||||||
|
"id": "0",
|
||||||
|
"info": Object {
|
||||||
|
"author": Object {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "url/to/GrafanaLabs",
|
||||||
|
},
|
||||||
|
"description": "pretty decent plugin",
|
||||||
|
"links": Array [
|
||||||
|
"one link",
|
||||||
|
],
|
||||||
|
"logos": Object {
|
||||||
|
"large": "large/logo",
|
||||||
|
"small": "small/logo",
|
||||||
|
},
|
||||||
|
"screenshots": "screenshot/0",
|
||||||
|
"updated": "2018-09-26",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"latestVersion": "1.0",
|
||||||
|
"name": "pretty cool plugin-0",
|
||||||
|
"pinned": false,
|
||||||
|
"state": "",
|
||||||
|
"type": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PluginListItem
|
||||||
|
key="pretty cool plugin-1-1"
|
||||||
|
plugin={
|
||||||
|
Object {
|
||||||
|
"defaultNavUrl": "some/url",
|
||||||
|
"enabled": false,
|
||||||
|
"hasUpdate": false,
|
||||||
|
"id": "1",
|
||||||
|
"info": Object {
|
||||||
|
"author": Object {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "url/to/GrafanaLabs",
|
||||||
|
},
|
||||||
|
"description": "pretty decent plugin",
|
||||||
|
"links": Array [
|
||||||
|
"one link",
|
||||||
|
],
|
||||||
|
"logos": Object {
|
||||||
|
"large": "large/logo",
|
||||||
|
"small": "small/logo",
|
||||||
|
},
|
||||||
|
"screenshots": "screenshot/1",
|
||||||
|
"updated": "2018-09-26",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"latestVersion": "1.1",
|
||||||
|
"name": "pretty cool plugin-1",
|
||||||
|
"pinned": false,
|
||||||
|
"state": "",
|
||||||
|
"type": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PluginListItem
|
||||||
|
key="pretty cool plugin-2-2"
|
||||||
|
plugin={
|
||||||
|
Object {
|
||||||
|
"defaultNavUrl": "some/url",
|
||||||
|
"enabled": false,
|
||||||
|
"hasUpdate": false,
|
||||||
|
"id": "2",
|
||||||
|
"info": Object {
|
||||||
|
"author": Object {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "url/to/GrafanaLabs",
|
||||||
|
},
|
||||||
|
"description": "pretty decent plugin",
|
||||||
|
"links": Array [
|
||||||
|
"one link",
|
||||||
|
],
|
||||||
|
"logos": Object {
|
||||||
|
"large": "large/logo",
|
||||||
|
"small": "small/logo",
|
||||||
|
},
|
||||||
|
"screenshots": "screenshot/2",
|
||||||
|
"updated": "2018-09-26",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"latestVersion": "1.2",
|
||||||
|
"name": "pretty cool plugin-2",
|
||||||
|
"pinned": false,
|
||||||
|
"state": "",
|
||||||
|
"type": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PluginListItem
|
||||||
|
key="pretty cool plugin-3-3"
|
||||||
|
plugin={
|
||||||
|
Object {
|
||||||
|
"defaultNavUrl": "some/url",
|
||||||
|
"enabled": false,
|
||||||
|
"hasUpdate": false,
|
||||||
|
"id": "3",
|
||||||
|
"info": Object {
|
||||||
|
"author": Object {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "url/to/GrafanaLabs",
|
||||||
|
},
|
||||||
|
"description": "pretty decent plugin",
|
||||||
|
"links": Array [
|
||||||
|
"one link",
|
||||||
|
],
|
||||||
|
"logos": Object {
|
||||||
|
"large": "large/logo",
|
||||||
|
"small": "small/logo",
|
||||||
|
},
|
||||||
|
"screenshots": "screenshot/3",
|
||||||
|
"updated": "2018-09-26",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"latestVersion": "1.3",
|
||||||
|
"name": "pretty cool plugin-3",
|
||||||
|
"pinned": false,
|
||||||
|
"state": "",
|
||||||
|
"type": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PluginListItem
|
||||||
|
key="pretty cool plugin-4-4"
|
||||||
|
plugin={
|
||||||
|
Object {
|
||||||
|
"defaultNavUrl": "some/url",
|
||||||
|
"enabled": false,
|
||||||
|
"hasUpdate": false,
|
||||||
|
"id": "4",
|
||||||
|
"info": Object {
|
||||||
|
"author": Object {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "url/to/GrafanaLabs",
|
||||||
|
},
|
||||||
|
"description": "pretty decent plugin",
|
||||||
|
"links": Array [
|
||||||
|
"one link",
|
||||||
|
],
|
||||||
|
"logos": Object {
|
||||||
|
"large": "large/logo",
|
||||||
|
"small": "small/logo",
|
||||||
|
},
|
||||||
|
"screenshots": "screenshot/4",
|
||||||
|
"updated": "2018-09-26",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"latestVersion": "1.4",
|
||||||
|
"name": "pretty cool plugin-4",
|
||||||
|
"pinned": false,
|
||||||
|
"state": "",
|
||||||
|
"type": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PluginListItem
|
||||||
|
key="pretty cool plugin-5-5"
|
||||||
|
plugin={
|
||||||
|
Object {
|
||||||
|
"defaultNavUrl": "some/url",
|
||||||
|
"enabled": false,
|
||||||
|
"hasUpdate": false,
|
||||||
|
"id": "5",
|
||||||
|
"info": Object {
|
||||||
|
"author": Object {
|
||||||
|
"name": "Grafana Labs",
|
||||||
|
"url": "url/to/GrafanaLabs",
|
||||||
|
},
|
||||||
|
"description": "pretty decent plugin",
|
||||||
|
"links": Array [
|
||||||
|
"one link",
|
||||||
|
],
|
||||||
|
"logos": Object {
|
||||||
|
"large": "large/logo",
|
||||||
|
"small": "small/logo",
|
||||||
|
},
|
||||||
|
"screenshots": "screenshot/5",
|
||||||
|
"updated": "2018-09-26",
|
||||||
|
"version": "1",
|
||||||
|
},
|
||||||
|
"latestVersion": "1.5",
|
||||||
|
"name": "pretty cool plugin-5",
|
||||||
|
"pinned": false,
|
||||||
|
"state": "",
|
||||||
|
"type": "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
`;
|
@ -0,0 +1,106 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<li
|
||||||
|
className="card-item-wrapper"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="card-item"
|
||||||
|
href="plugins/1/edit"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-type"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="icon-gf icon-gf-"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="card-item-body"
|
||||||
|
>
|
||||||
|
<figure
|
||||||
|
className="card-item-figure"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="small/logo"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<div
|
||||||
|
className="card-item-details"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-name"
|
||||||
|
>
|
||||||
|
pretty cool plugin 1
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="card-item-sub-name"
|
||||||
|
>
|
||||||
|
By Grafana Labs
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render has plugin section 1`] = `
|
||||||
|
<li
|
||||||
|
className="card-item-wrapper"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className="card-item"
|
||||||
|
href="plugins/1/edit"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-type"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="icon-gf icon-gf-"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="card-item-notice"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
bs-tooltip="plugin.latestVersion"
|
||||||
|
>
|
||||||
|
Update available!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="card-item-body"
|
||||||
|
>
|
||||||
|
<figure
|
||||||
|
className="card-item-figure"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="small/logo"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
<div
|
||||||
|
className="card-item-details"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="card-item-name"
|
||||||
|
>
|
||||||
|
pretty cool plugin 1
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="card-item-sub-name"
|
||||||
|
>
|
||||||
|
By Grafana Labs
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`;
|
@ -0,0 +1,18 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
model={Object {}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="page-container page-body"
|
||||||
|
>
|
||||||
|
<Connect(PluginActionBar) />
|
||||||
|
<PluginList
|
||||||
|
layoutMode="grid"
|
||||||
|
plugins={Array []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -1,6 +1,5 @@
|
|||||||
import './plugin_edit_ctrl';
|
import './plugin_edit_ctrl';
|
||||||
import './plugin_page_ctrl';
|
import './plugin_page_ctrl';
|
||||||
import './plugin_list_ctrl';
|
|
||||||
import './import_list/import_list';
|
import './import_list/import_list';
|
||||||
import './ds_edit_ctrl';
|
import './ds_edit_ctrl';
|
||||||
import './ds_dashboards_ctrl';
|
import './ds_dashboards_ctrl';
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
<page-header model="ctrl.navModel"></page-header>
|
|
||||||
|
|
||||||
<div class="page-container page-body">
|
|
||||||
<div class="page-action-bar">
|
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<label class="gf-form--has-input-icon">
|
|
||||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.searchQuery" ng-change="ctrl.onQueryUpdated()" placeholder="Filter by name or type" />
|
|
||||||
<i class="gf-form-input-icon fa fa-search"></i>
|
|
||||||
</label>
|
|
||||||
<layout-selector />
|
|
||||||
</div>
|
|
||||||
<div class="page-action-bar__spacer"></div>
|
|
||||||
<a class="btn btn-success" href="https://grafana.com/plugins?utm_source=grafana_plugin_list" target="_blank">
|
|
||||||
Find more plugins on Grafana.com
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="card-section" layout-mode>
|
|
||||||
|
|
||||||
<ol class="card-list" >
|
|
||||||
<li class="card-item-wrapper" ng-repeat="plugin in ctrl.plugins">
|
|
||||||
<a class="card-item" href="plugins/{{plugin.id}}/edit">
|
|
||||||
<div class="card-item-header">
|
|
||||||
<div class="card-item-type">
|
|
||||||
<i class="icon-gf icon-gf-{{plugin.type}}"></i>
|
|
||||||
{{plugin.type}}
|
|
||||||
</div>
|
|
||||||
<div class="card-item-notice" ng-show="plugin.hasUpdate">
|
|
||||||
<span bs-tooltip="plugin.latestVersion">Update available!</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-item-body">
|
|
||||||
<figure class="card-item-figure">
|
|
||||||
<img ng-src="{{plugin.info.logos.small}}">
|
|
||||||
</figure>
|
|
||||||
<div class="card-item-details">
|
|
||||||
<div class="card-item-name">{{plugin.name}}</div>
|
|
||||||
<div class="card-item-sub-name">By {{plugin.info.author.name}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
@ -1,30 +0,0 @@
|
|||||||
import angular from 'angular';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export class PluginListCtrl {
|
|
||||||
plugins: any[];
|
|
||||||
tabIndex: number;
|
|
||||||
navModel: any;
|
|
||||||
searchQuery: string;
|
|
||||||
allPlugins: any[];
|
|
||||||
|
|
||||||
/** @ngInject */
|
|
||||||
constructor(private backendSrv: any, $location, navModelSrv) {
|
|
||||||
this.tabIndex = 0;
|
|
||||||
this.navModel = navModelSrv.getNav('cfg', 'plugins', 0);
|
|
||||||
|
|
||||||
this.backendSrv.get('api/plugins', { embedded: 0 }).then(plugins => {
|
|
||||||
this.plugins = plugins;
|
|
||||||
this.allPlugins = plugins;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onQueryUpdated() {
|
|
||||||
const regex = new RegExp(this.searchQuery, 'ig');
|
|
||||||
this.plugins = _.filter(this.allPlugins, item => {
|
|
||||||
return regex.test(item.name) || regex.test(item.type);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
angular.module('grafana.controllers').controller('PluginListCtrl', PluginListCtrl);
|
|
51
public/app/features/plugins/state/actions.ts
Normal file
51
public/app/features/plugins/state/actions.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Plugin, StoreState } from 'app/types';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
import { getBackendSrv } from '../../../core/services/backend_srv';
|
||||||
|
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||||
|
|
||||||
|
export enum ActionTypes {
|
||||||
|
LoadPlugins = 'LOAD_PLUGINS',
|
||||||
|
SetPluginsSearchQuery = 'SET_PLUGIN_SEARCH_QUERY',
|
||||||
|
SetLayoutMode = 'SET_LAYOUT_MODE',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadPluginsAction {
|
||||||
|
type: ActionTypes.LoadPlugins;
|
||||||
|
payload: Plugin[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetPluginsSearchQueryAction {
|
||||||
|
type: ActionTypes.SetPluginsSearchQuery;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetLayoutModeAction {
|
||||||
|
type: ActionTypes.SetLayoutMode;
|
||||||
|
payload: LayoutMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setLayoutMode = (mode: LayoutMode): SetLayoutModeAction => ({
|
||||||
|
type: ActionTypes.SetLayoutMode,
|
||||||
|
payload: mode,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setPluginsSearchQuery = (query: string): SetPluginsSearchQueryAction => ({
|
||||||
|
type: ActionTypes.SetPluginsSearchQuery,
|
||||||
|
payload: query,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({
|
||||||
|
type: ActionTypes.LoadPlugins,
|
||||||
|
payload: plugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Action = LoadPluginsAction | SetPluginsSearchQueryAction | SetLayoutModeAction;
|
||||||
|
|
||||||
|
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||||
|
|
||||||
|
export function loadPlugins(): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const result = await getBackendSrv().get('api/plugins', { embedded: 0 });
|
||||||
|
dispatch(pluginsLoaded(result));
|
||||||
|
};
|
||||||
|
}
|
27
public/app/features/plugins/state/reducers.ts
Normal file
27
public/app/features/plugins/state/reducers.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Action, ActionTypes } from './actions';
|
||||||
|
import { Plugin, PluginsState } from 'app/types';
|
||||||
|
import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||||
|
|
||||||
|
export const initialState: PluginsState = {
|
||||||
|
plugins: [] as Plugin[],
|
||||||
|
searchQuery: '',
|
||||||
|
layoutMode: LayoutModes.Grid,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pluginsReducer = (state = initialState, action: Action): PluginsState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.LoadPlugins:
|
||||||
|
return { ...state, plugins: action.payload };
|
||||||
|
|
||||||
|
case ActionTypes.SetPluginsSearchQuery:
|
||||||
|
return { ...state, searchQuery: action.payload };
|
||||||
|
|
||||||
|
case ActionTypes.SetLayoutMode:
|
||||||
|
return { ...state, layoutMode: action.payload };
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
plugins: pluginsReducer,
|
||||||
|
};
|
31
public/app/features/plugins/state/selectors.test.ts
Normal file
31
public/app/features/plugins/state/selectors.test.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { getPlugins, getPluginsSearchQuery } from './selectors';
|
||||||
|
import { initialState } from './reducers';
|
||||||
|
import { getMockPlugins } from '../__mocks__/pluginMocks';
|
||||||
|
|
||||||
|
describe('Selectors', () => {
|
||||||
|
const mockState = initialState;
|
||||||
|
|
||||||
|
it('should return search query', () => {
|
||||||
|
mockState.searchQuery = 'test';
|
||||||
|
const query = getPluginsSearchQuery(mockState);
|
||||||
|
|
||||||
|
expect(query).toEqual(mockState.searchQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return plugins', () => {
|
||||||
|
mockState.plugins = getMockPlugins(5);
|
||||||
|
mockState.searchQuery = '';
|
||||||
|
|
||||||
|
const plugins = getPlugins(mockState);
|
||||||
|
|
||||||
|
expect(plugins).toEqual(mockState.plugins);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter plugins', () => {
|
||||||
|
mockState.searchQuery = 'plugin-1';
|
||||||
|
|
||||||
|
const plugins = getPlugins(mockState);
|
||||||
|
|
||||||
|
expect(plugins.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
10
public/app/features/plugins/state/selectors.ts
Normal file
10
public/app/features/plugins/state/selectors.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export const getPlugins = state => {
|
||||||
|
const regex = new RegExp(state.searchQuery, 'i');
|
||||||
|
|
||||||
|
return state.plugins.filter(item => {
|
||||||
|
return regex.test(item.name) || regex.test(item.info.author.name) || regex.test(item.info.description);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPluginsSearchQuery = state => state.searchQuery;
|
||||||
|
export const getLayoutMode = state => state.layoutMode;
|
58
public/app/features/templating/TextBoxVariable.ts
Normal file
58
public/app/features/templating/TextBoxVariable.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Variable, assignModelProperties, variableTypes } from './variable';
|
||||||
|
|
||||||
|
export class TextBoxVariable implements Variable {
|
||||||
|
query: string;
|
||||||
|
current: any;
|
||||||
|
options: any[];
|
||||||
|
skipUrlSync: boolean;
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
type: 'textbox',
|
||||||
|
name: '',
|
||||||
|
hide: 2,
|
||||||
|
label: '',
|
||||||
|
query: '',
|
||||||
|
current: {},
|
||||||
|
options: [],
|
||||||
|
skipUrlSync: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor(private model, private variableSrv) {
|
||||||
|
assignModelProperties(this, model, this.defaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSaveModel() {
|
||||||
|
assignModelProperties(this.model, this, this.defaults);
|
||||||
|
return this.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValue(option) {
|
||||||
|
this.variableSrv.setOptionAsCurrent(this, option);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOptions() {
|
||||||
|
this.options = [{ text: this.query.trim(), value: this.query.trim() }];
|
||||||
|
this.current = this.options[0];
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
dependsOn(variable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setValueFromUrl(urlValue) {
|
||||||
|
this.query = urlValue;
|
||||||
|
return this.variableSrv.setOptionFromUrl(this, urlValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
getValueForUrl() {
|
||||||
|
return this.current.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variableTypes['textbox'] = {
|
||||||
|
name: 'Text box',
|
||||||
|
ctor: TextBoxVariable,
|
||||||
|
description: 'Define a textbox variable, where users can enter any arbitrary string',
|
||||||
|
};
|
@ -9,6 +9,7 @@ import { DatasourceVariable } from './datasource_variable';
|
|||||||
import { CustomVariable } from './custom_variable';
|
import { CustomVariable } from './custom_variable';
|
||||||
import { ConstantVariable } from './constant_variable';
|
import { ConstantVariable } from './constant_variable';
|
||||||
import { AdhocVariable } from './adhoc_variable';
|
import { AdhocVariable } from './adhoc_variable';
|
||||||
|
import { TextBoxVariable } from './TextBoxVariable';
|
||||||
|
|
||||||
coreModule.factory('templateSrv', () => {
|
coreModule.factory('templateSrv', () => {
|
||||||
return templateSrv;
|
return templateSrv;
|
||||||
@ -22,4 +23,5 @@ export {
|
|||||||
CustomVariable,
|
CustomVariable,
|
||||||
ConstantVariable,
|
ConstantVariable,
|
||||||
AdhocVariable,
|
AdhocVariable,
|
||||||
|
TextBoxVariable,
|
||||||
};
|
};
|
||||||
|
@ -155,6 +155,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="current.type === 'textbox'" class="gf-form-group">
|
||||||
|
<h5 class="section-heading">Text options</h5>
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label">Default value</span>
|
||||||
|
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="default value, if any"></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div ng-if="current.type === 'query'" class="gf-form-group">
|
<div ng-if="current.type === 'query'" class="gf-form-group">
|
||||||
<h5 class="section-heading">Query Options</h5>
|
<h5 class="section-heading">Query Options</h5>
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-13">Default Region</label>
|
<label class="gf-form-label width-13">Default Region</label>
|
||||||
<div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon">
|
<div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon">
|
||||||
<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ca-central-1', 'cn-north-1', 'cn-northwest-1', 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-gov-west-1', 'us-west-1', 'us-west-2']"></select>
|
<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'ca-central-1', 'cn-north-1', 'cn-northwest-1', 'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-gov-west-1', 'us-west-1', 'us-west-2', 'us-isob-east-1', 'us-iso-east-1']"></select>
|
||||||
<info-popover mode="right-absolute">
|
<info-popover mode="right-absolute">
|
||||||
Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
|
Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
|
||||||
</info-popover>
|
</info-popover>
|
||||||
|
@ -5,6 +5,7 @@ import ServerStats from 'app/features/admin/ServerStats';
|
|||||||
import AlertRuleList from 'app/features/alerting/AlertRuleList';
|
import AlertRuleList from 'app/features/alerting/AlertRuleList';
|
||||||
import TeamPages from 'app/features/teams/TeamPages';
|
import TeamPages from 'app/features/teams/TeamPages';
|
||||||
import TeamList from 'app/features/teams/TeamList';
|
import TeamList from 'app/features/teams/TeamList';
|
||||||
|
import PluginListPage from 'app/features/plugins/PluginListPage';
|
||||||
import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
|
import FolderSettingsPage from 'app/features/folders/FolderSettingsPage';
|
||||||
import FolderPermissions from 'app/features/folders/FolderPermissions';
|
import FolderPermissions from 'app/features/folders/FolderPermissions';
|
||||||
|
|
||||||
@ -245,9 +246,10 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
controllerAs: 'ctrl',
|
controllerAs: 'ctrl',
|
||||||
})
|
})
|
||||||
.when('/plugins', {
|
.when('/plugins', {
|
||||||
templateUrl: 'public/app/features/plugins/partials/plugin_list.html',
|
template: '<react-container />',
|
||||||
controller: 'PluginListCtrl',
|
resolve: {
|
||||||
controllerAs: 'ctrl',
|
component: () => PluginListPage,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.when('/plugins/:pluginId/edit', {
|
.when('/plugins/:pluginId/edit', {
|
||||||
templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',
|
templateUrl: 'public/app/features/plugins/partials/plugin_edit.html',
|
||||||
|
@ -6,6 +6,7 @@ import alertingReducers from 'app/features/alerting/state/reducers';
|
|||||||
import teamsReducers from 'app/features/teams/state/reducers';
|
import teamsReducers from 'app/features/teams/state/reducers';
|
||||||
import foldersReducers from 'app/features/folders/state/reducers';
|
import foldersReducers from 'app/features/folders/state/reducers';
|
||||||
import dashboardReducers from 'app/features/dashboard/state/reducers';
|
import dashboardReducers from 'app/features/dashboard/state/reducers';
|
||||||
|
import pluginReducers from 'app/features/plugins/state/reducers';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
...sharedReducers,
|
...sharedReducers,
|
||||||
@ -13,6 +14,7 @@ const rootReducer = combineReducers({
|
|||||||
...teamsReducers,
|
...teamsReducers,
|
||||||
...foldersReducers,
|
...foldersReducers,
|
||||||
...dashboardReducers,
|
...dashboardReducers,
|
||||||
|
...pluginReducers,
|
||||||
});
|
});
|
||||||
|
|
||||||
export let store;
|
export let store;
|
||||||
|
@ -6,7 +6,7 @@ import { FolderDTO, FolderState, FolderInfo } from './folders';
|
|||||||
import { DashboardState } from './dashboard';
|
import { DashboardState } from './dashboard';
|
||||||
import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
|
import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
|
||||||
import { DataSource } from './datasources';
|
import { DataSource } from './datasources';
|
||||||
import { PluginMeta } from './plugins';
|
import { PluginMeta, Plugin, PluginsState } from './plugins';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Team,
|
Team,
|
||||||
@ -33,6 +33,8 @@ export {
|
|||||||
PermissionLevel,
|
PermissionLevel,
|
||||||
DataSource,
|
DataSource,
|
||||||
PluginMeta,
|
PluginMeta,
|
||||||
|
Plugin,
|
||||||
|
PluginsState,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
|
@ -12,8 +12,36 @@ export interface PluginInclude {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginMetaInfo {
|
export interface PluginMetaInfo {
|
||||||
|
author: {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
description: string;
|
||||||
|
links: string[];
|
||||||
logos: {
|
logos: {
|
||||||
large: string;
|
large: string;
|
||||||
small: string;
|
small: string;
|
||||||
};
|
};
|
||||||
|
screenshots: string;
|
||||||
|
updated: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Plugin {
|
||||||
|
defaultNavUrl: string;
|
||||||
|
enabled: boolean;
|
||||||
|
hasUpdate: boolean;
|
||||||
|
id: string;
|
||||||
|
info: PluginMetaInfo;
|
||||||
|
latestVersion: string;
|
||||||
|
name: string;
|
||||||
|
pinned: boolean;
|
||||||
|
state: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginsState {
|
||||||
|
plugins: Plugin[];
|
||||||
|
searchQuery: string;
|
||||||
|
layoutMode: string;
|
||||||
}
|
}
|
||||||
|
@ -221,6 +221,31 @@ $btn-service-icon-width: 35px;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Toggle button
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
background: $input-label-bg;
|
||||||
|
color: $text-color-weak;
|
||||||
|
box-shadow: $card-shadow;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-radius: 2px 0 0 2px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: lighten($input-label-bg, 5%);
|
||||||
|
color: $link-color;
|
||||||
|
&:hover {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//Button animations
|
//Button animations
|
||||||
|
|
||||||
.btn-loading span {
|
.btn-loading span {
|
||||||
|
@ -3,7 +3,7 @@ $select-menu-max-height: 300px;
|
|||||||
$select-item-font-size: $font-size-base;
|
$select-item-font-size: $font-size-base;
|
||||||
$select-item-bg: $dropdownBackground;
|
$select-item-bg: $dropdownBackground;
|
||||||
$select-item-fg: $input-color;
|
$select-item-fg: $input-color;
|
||||||
$select-option-bg: $dropdownBackground;
|
$select-option-bg: $menu-dropdown-bg;
|
||||||
$select-option-color: $input-color;
|
$select-option-color: $input-color;
|
||||||
$select-noresults-color: $text-color;
|
$select-noresults-color: $text-color;
|
||||||
$select-input-bg: $input-bg;
|
$select-input-bg: $input-bg;
|
||||||
@ -82,20 +82,14 @@ $select-option-selected-bg: $dropdownLinkBackgroundActive;
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Select-option {
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.Select-option.is-focused {
|
.Select-option.is-focused {
|
||||||
background-color: $dropdownLinkBackgroundHover;
|
background-color: $dropdownLinkBackgroundHover;
|
||||||
color: $dropdownLinkColorHover;
|
color: $dropdownLinkColorHover;
|
||||||
|
@include left-brand-border-gradient();
|
||||||
&::before {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
|
||||||
width: 2px;
|
|
||||||
display: block;
|
|
||||||
content: '';
|
|
||||||
background-image: linear-gradient(to bottom, #ffd500 0%, #ff4400 99%, #ff4400 100%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.datasource-picker {
|
.datasource-picker {
|
||||||
min-width: 10rem;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timepicker {
|
.timepicker {
|
||||||
|
@ -275,7 +275,10 @@
|
|||||||
document.head.insertBefore(myCSS, document.head.childNodes[document.head.childNodes.length - 1].nextSibling);
|
document.head.insertBefore(myCSS, document.head.childNodes[document.head.childNodes.length - 1].nextSibling);
|
||||||
// switch loader to show all has loaded
|
// switch loader to show all has loaded
|
||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
document.getElementsByClassName("preloader")[0].className = "preloader preloader--done";
|
var preloader = document.getElementsByClassName("preloader");
|
||||||
|
if (preloader.length) {
|
||||||
|
preloader[0].className = "preloader preloader--done";
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Lint and build CSS
|
// Lint and build CSS
|
||||||
module.exports = function(grunt) {
|
module.exports = function (grunt) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
grunt.registerTask('default', [
|
grunt.registerTask('default', [
|
||||||
@ -18,15 +18,16 @@ module.exports = function(grunt) {
|
|||||||
grunt.registerTask('precommit', [
|
grunt.registerTask('precommit', [
|
||||||
'sasslint',
|
'sasslint',
|
||||||
'exec:tslint',
|
'exec:tslint',
|
||||||
|
'exec:tsc',
|
||||||
'no-only-tests'
|
'no-only-tests'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
grunt.registerTask('no-only-tests', function() {
|
grunt.registerTask('no-only-tests', function () {
|
||||||
var files = grunt.file.expand('public/**/*_specs\.ts', 'public/**/*_specs\.js');
|
var files = grunt.file.expand('public/**/*_specs\.ts', 'public/**/*_specs\.js');
|
||||||
|
|
||||||
files.forEach(function(spec) {
|
files.forEach(function (spec) {
|
||||||
var rows = grunt.file.read(spec).split('\n');
|
var rows = grunt.file.read(spec).split('\n');
|
||||||
rows.forEach(function(row) {
|
rows.forEach(function (row) {
|
||||||
if (row.indexOf('.only(') > 0) {
|
if (row.indexOf('.only(') > 0) {
|
||||||
grunt.log.errorlns(row);
|
grunt.log.errorlns(row);
|
||||||
grunt.fail.warn('found only statement in test: ' + spec)
|
grunt.fail.warn('found only statement in test: ' + spec)
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
module.exports = function(config, grunt) {
|
module.exports = function (config, grunt) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tslint: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json',
|
tslint: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json',
|
||||||
|
tsc: 'yarn tsc --noEmit',
|
||||||
jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2',
|
jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2',
|
||||||
webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js',
|
webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js',
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user