mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
24a165662c
12
.flooignore
12
.flooignore
@ -1,12 +0,0 @@
|
||||
#*
|
||||
*.o
|
||||
*.pyc
|
||||
*.pyo
|
||||
*~
|
||||
extern/
|
||||
node_modules/
|
||||
tmp/
|
||||
data/
|
||||
vendor/
|
||||
public_gen/
|
||||
dist/
|
22
CHANGELOG.md
22
CHANGELOG.md
@ -2,17 +2,21 @@
|
||||
|
||||
### WIP (in develop branch currently as its unstable or unfinished)
|
||||
- Dashboard folders
|
||||
- User groups
|
||||
- User groups
|
||||
- Dashboard permissions (on folder & dashboard level), permissions can be assigned to groups or individual users
|
||||
- UX changes to nav & side menu
|
||||
- New dashboard grid layout system
|
||||
|
||||
# 4.5.0 (unreleased)
|
||||
|
||||
# 4.5.0-beta1 (2017-09-05)
|
||||
|
||||
## New Features
|
||||
|
||||
* **Table panel**: Render cell values as links that can have an url template that uses variables from current table row. [#3754](https://github.com/grafana/grafana/issues/3754)
|
||||
* **Elasticsearch**: Add ad hoc filters directly by clicking values in table panel [#8052](https://github.com/grafana/grafana/issues/8052).
|
||||
* **MySQL**: New rich query editor with syntax highlighting
|
||||
* **Prometheus**: New rich query editor with syntax highlighting, metric & range auto complete and integrated function docs. [#5117](https://github.com/grafana/grafana/issues/5117)
|
||||
|
||||
## Enhancements
|
||||
|
||||
@ -20,20 +24,20 @@
|
||||
* **Graphite**: Calls to Graphite api /metrics/find now include panel or dashboad time range (from & until) in most cases, [#8055](https://github.com/grafana/grafana/issues/8055)
|
||||
* **Graphite**: Added new graphite 1.0 functions, available if you set version to 1.0.x in data source settings. New Functions: mapSeries, reduceSeries, isNonNull, groupByNodes, offsetToZero, grep, weightedAverage, removeEmptySeries, aggregateLine, averageOutsidePercentile, delay, exponentialMovingAverage, fallbackSeries, integralByInterval, interpolate, invert, linearRegression, movingMin, movingMax, movingSum, multiplySeriesWithWildcards, pow, powSeries, removeBetweenPercentile, squareRoot, timeSlice, closes [#8261](https://github.com/grafana/grafana/issues/8261)
|
||||
- **Elasticsearch**: Ad-hoc filters now use query phrase match filters instead of term filters, works on non keyword/raw fields [#9095](https://github.com/grafana/grafana/issues/9095).
|
||||
|
||||
|
||||
### Breaking change
|
||||
|
||||
* **InfluxDB/Elasticsearch**: The panel & data source option named "Group by time interval" is now named "Min time interval" and does now always define a lower limit for the auto group by time. Without having to use `>` prefix (that prefix still works). This should in theory have close to zero actual impact on existing dashboards. It does mean that if you used this setting to define a hard group by time interval of, say "1d", if you zoomed to a time range wide enough the time range could increase above the "1d" range as the setting is now always considered a lower limit.
|
||||
|
||||
## Changes
|
||||
|
||||
* **InfluxDB**: Change time range filter for absolute time ranges to be inclusive instead of exclusive [#8319](https://github.com/grafana/grafana/issues/8319), thx [@Oxydros](https://github.com/Oxydros)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* **Modals**: Maintain scroll position after opening/leaving modal [#8800](https://github.com/grafana/grafana/issues/8800)
|
||||
* **Templating**: You cannot select data source variables as data source for other template variables [#7510](https://github.com/grafana/grafana/issues/7510)
|
||||
|
||||
# 4.4.4 (unreleased)
|
||||
* **InfluxDB**: Added paranthesis around tag filters in queries [#9131](https://github.com/grafana/grafana/pull/9131)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* **Modals**: Maintain scroll position after opening/leaving modal [#8800](https://github.com/grafana/grafana/issues/8800)
|
||||
* **Templating**: You cannot select data source variables as data source for other template variables [#7510](https://github.com/grafana/grafana/issues/7510)
|
||||
* **MySQL/Postgres**: Fix for max_idle_conn option default which was wrongly set to zero which does not mean unlimited but means zero, which in practice kind of disables connection pooling, which is not good. Fixes [#8513](https://github.com/grafana/grafana/issues/8513)
|
||||
|
||||
# 4.4.3 (2017-08-07)
|
||||
|
@ -1,4 +1,4 @@
|
||||
[Grafana](https://grafana.com) [](https://circleci.com/gh/grafana/grafana)
|
||||
[Grafana](https://grafana.com) [](https://circleci.com/gh/grafana/grafana) [](https://goreportcard.com/report/github.com/grafana/grafana)
|
||||
================
|
||||
[Website](https://grafana.com) |
|
||||
[Twitter](https://twitter.com/grafana) |
|
||||
@ -124,7 +124,7 @@ To build the frontend assets only on changes:
|
||||
|
||||
```bash
|
||||
sudo npm install -g grunt-cli # to do only once to install grunt command line interface
|
||||
grunt watch
|
||||
grunt && grunt watch
|
||||
```
|
||||
|
||||
### Recompile backend on source change
|
||||
|
22
ROADMAP.md
22
ROADMAP.md
@ -1,31 +1,29 @@
|
||||
# Roadmap (2017-04-23)
|
||||
# Roadmap (2017-08-29)
|
||||
|
||||
This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change.
|
||||
But it will give you an idea of our current vision and plan.
|
||||
|
||||
### Short term (1-4 months)
|
||||
|
||||
- New Heatmap Panel (Implemented and available in master)
|
||||
- Support for MySQL & Postgres as data sources (Work started and a alpha version for MySQL is available in master)
|
||||
- User Groups & Dashboard folders with ACLs (work started, not yet completed, https://github.com/grafana/grafana/issues/1611#issuecomment-287742633)
|
||||
- Improve new user UX
|
||||
- Improve docs
|
||||
- Support for alerting for Elasticsearch (can be tested in [branch](https://github.com/grafana/grafana/tree/alerting-elasticsearch) but needs more work)
|
||||
- Graph annotations (create from grafana, region annotations, better annotation viz)
|
||||
- Improve alerting (clustering, silence rules)
|
||||
- Release Grafana v4.5 with fixes and minor enhancements
|
||||
- Release Grafana v5
|
||||
- User groups
|
||||
- Dashboard folders
|
||||
- Dashboard permissions (on folders as well), permissions on groups or users
|
||||
- New Dashboard layout engine
|
||||
- New sidemenu & nav UX
|
||||
- Elasticsearch alerting
|
||||
|
||||
### Long term
|
||||
|
||||
- Improved dashboard panel layout engine (to make it easier and enable more flexible layouts)
|
||||
- Backend plugins to support more Auth options, Alerting data sources & notifications
|
||||
- Universial time series transformations for any data source (meta queries)
|
||||
- Reporting
|
||||
- Web socket & live data streams
|
||||
- Migrate to Angular2
|
||||
- Migrate to Angular2 or react
|
||||
|
||||
|
||||
### Outside contributions
|
||||
We know this is being worked on right now by contributors (and we hope to merge it when it's ready).
|
||||
|
||||
- Dashboard revisions (be able to revert dashboard changes)
|
||||
- Clustering for alert engine (load distribution)
|
||||
|
@ -1 +1 @@
|
||||
v4.2
|
||||
v4.3
|
||||
|
@ -13,6 +13,7 @@ Here you can find links to older versions of the documentation that might be bet
|
||||
of Grafana.
|
||||
|
||||
- [Latest](http://docs.grafana.org)
|
||||
- [Version 4.3](http://docs.grafana.org/v4.3)
|
||||
- [Version 4.2](http://docs.grafana.org/v4.2)
|
||||
- [Version 4.1](http://docs.grafana.org/v4.1)
|
||||
- [Version 4.0](http://docs.grafana.org/v4.0)
|
||||
|
@ -11,14 +11,16 @@ parent = "http_api"
|
||||
|
||||
# Admin API
|
||||
|
||||
The admin http API does not currently work with an api token. Api Token's are currently only linked to an organization and organization role. They cannot given
|
||||
the permission of server admin, only user's can be given that permission. So in order to use these API calls you will have to use basic auth and Grafana user
|
||||
with Grafana admin permission.
|
||||
The Admin HTTP API does not currently work with an API Token. API Tokens are currently only linked to an organization and an organization role. They cannot be given
|
||||
the permission of server admin, only users can be given that permission. So in order to use these API calls you will have to use Basic Auth and the Grafana user
|
||||
must have the Grafana Admin permission. (The default admin user is called `admin` and has permission to use this API.)
|
||||
|
||||
## Settings
|
||||
|
||||
`GET /api/admin/settings`
|
||||
|
||||
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/admin/settings
|
||||
@ -176,6 +178,8 @@ with Grafana admin permission.
|
||||
|
||||
`GET /api/admin/stats`
|
||||
|
||||
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
GET /api/admin/stats
|
||||
@ -203,7 +207,7 @@ with Grafana admin permission.
|
||||
|
||||
`POST /api/admin/users`
|
||||
|
||||
Create new user
|
||||
Create new user. Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
@ -229,7 +233,8 @@ Create new user
|
||||
|
||||
`PUT /api/admin/users/:id/password`
|
||||
|
||||
Change password for specific user
|
||||
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
|
||||
Change password for a specific user.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
@ -250,6 +255,8 @@ Change password for specific user
|
||||
|
||||
`PUT /api/admin/users/:id/permissions`
|
||||
|
||||
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
PUT /api/admin/users/2/permissions HTTP/1.1
|
||||
@ -269,6 +276,8 @@ Change password for specific user
|
||||
|
||||
`DELETE /api/admin/users/:id`
|
||||
|
||||
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
DELETE /api/admin/users/2 HTTP/1.1
|
||||
@ -286,6 +295,8 @@ Change password for specific user
|
||||
|
||||
`POST /api/admin/pause-all-alerts`
|
||||
|
||||
Only works with Basic Authentication (username and password). See [introduction](http://docs.grafana.org/http_api/admin/#admin-api) for an explanation.
|
||||
|
||||
**Example Request**:
|
||||
|
||||
POST /api/admin/pause-all-alerts HTTP/1.1
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"author": {
|
||||
"name": "Torkel Ödegaard",
|
||||
"company": "Coding Instinct AB"
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "5.0.0-pre1",
|
||||
"version": "4.5.0-beta1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
@ -63,6 +63,7 @@
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.2.8",
|
||||
"eventemitter3": "^2.0.2",
|
||||
"gaze": "^1.1.2",
|
||||
"grunt-jscs": "3.0.1",
|
||||
|
@ -6,6 +6,7 @@ set -e
|
||||
|
||||
IS_UPGRADE=false
|
||||
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
[ -z "$GRAFANA_USER" ] && GRAFANA_USER="grafana"
|
||||
|
@ -17,3 +17,6 @@ CONF_FILE=/etc/grafana/grafana.ini
|
||||
RESTART_ON_UPGRADE=true
|
||||
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
|
||||
# Only used on systemd systems
|
||||
PID_FILE_DIR=/var/run/grafana
|
||||
|
@ -12,11 +12,13 @@ Group=grafana
|
||||
Type=simple
|
||||
Restart=on-failure
|
||||
WorkingDirectory=/usr/share/grafana
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE} \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
RuntimeDirectory=grafana
|
||||
RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR}
|
||||
LimitNOFILE=10000
|
||||
TimeoutStopSec=20
|
||||
|
@ -25,6 +25,7 @@ stopGrafana() {
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Initial installation: $1 == 1
|
||||
# Upgrade: $1 == 2, and configured to restart on upgrade
|
||||
if [ $1 -eq 1 ] ; then
|
||||
|
@ -17,3 +17,6 @@ CONF_FILE=/etc/grafana/grafana.ini
|
||||
RESTART_ON_UPGRADE=true
|
||||
|
||||
PLUGINS_DIR=/var/lib/grafana/plugins
|
||||
|
||||
# Only used on systemd systems
|
||||
PID_FILE_DIR=/var/run/grafana
|
||||
|
@ -12,11 +12,13 @@ Group=grafana
|
||||
Type=simple
|
||||
Restart=on-failure
|
||||
WorkingDirectory=/usr/share/grafana
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE} \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
RuntimeDirectory=grafana
|
||||
RuntimeDirectoryMode=0750
|
||||
ExecStart=/usr/sbin/grafana-server \
|
||||
--config=${CONF_FILE} \
|
||||
--pidfile=${PID_FILE_DIR}/grafana-server.pid \
|
||||
cfg:default.paths.logs=${LOG_DIR} \
|
||||
cfg:default.paths.data=${DATA_DIR} \
|
||||
cfg:default.paths.plugins=${PLUGINS_DIR}
|
||||
LimitNOFILE=10000
|
||||
TimeoutStopSec=20
|
||||
|
@ -209,7 +209,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
|
||||
r.Get("/plugins", wrap(GetPluginList))
|
||||
r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById))
|
||||
r.Get("/plugins/:pluginId/readme", wrap(GetPluginReadme))
|
||||
r.Get("/plugins/:pluginId/markdown/:name", wrap(GetPluginMarkdown))
|
||||
|
||||
r.Group("/plugins", func() {
|
||||
r.Get("/:pluginId/dashboards/", wrap(GetPluginDashboards))
|
||||
|
@ -91,7 +91,7 @@ func init() {
|
||||
"AWS/SWF": {"DecisionTaskScheduleToStartTime", "DecisionTaskStartToCloseTime", "DecisionTasksCompleted", "StartedDecisionTasksTimedOutOnClose", "WorkflowStartToCloseTime", "WorkflowsCanceled", "WorkflowsCompleted", "WorkflowsContinuedAsNew", "WorkflowsFailed", "WorkflowsTerminated", "WorkflowsTimedOut",
|
||||
"ActivityTaskScheduleToCloseTime", "ActivityTaskScheduleToStartTime", "ActivityTaskStartToCloseTime", "ActivityTasksCanceled", "ActivityTasksCompleted", "ActivityTasksFailed", "ScheduledActivityTasksTimedOutOnClose", "ScheduledActivityTasksTimedOutOnStart", "StartedActivityTasksTimedOutOnClose", "StartedActivityTasksTimedOutOnHeartbeat"},
|
||||
"AWS/VPN": {"TunnelState", "TunnelDataIn", "TunnelDataOut"},
|
||||
"AWS/WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
|
||||
"WAF": {"AllowedRequests", "BlockedRequests", "CountedRequests"},
|
||||
"AWS/WorkSpaces": {"Available", "Unhealthy", "ConnectionAttempt", "ConnectionSuccess", "ConnectionFailure", "SessionLaunchTime", "InSessionLatency", "SessionDisconnect"},
|
||||
"KMS": {"SecondsUntilKeyMaterialExpiration"},
|
||||
}
|
||||
@ -133,7 +133,7 @@ func init() {
|
||||
"AWS/StorageGateway": {"GatewayId", "GatewayName", "VolumeId"},
|
||||
"AWS/SWF": {"Domain", "WorkflowTypeName", "WorkflowTypeVersion", "ActivityTypeName", "ActivityTypeVersion"},
|
||||
"AWS/VPN": {"VpnId", "TunnelIpAddress"},
|
||||
"AWS/WAF": {"Rule", "WebACL"},
|
||||
"WAF": {"Rule", "WebACL"},
|
||||
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
|
||||
"KMS": {"KeyId"},
|
||||
}
|
||||
@ -292,11 +292,6 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
|
||||
var metricsCacheLock sync.Mutex
|
||||
|
||||
func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
|
||||
result, err := getAllMetrics(dsInfo)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
metricsCacheLock.Lock()
|
||||
defer metricsCacheLock.Unlock()
|
||||
|
||||
@ -314,6 +309,10 @@ func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*data
|
||||
if customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) {
|
||||
return customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
|
||||
}
|
||||
result, err := getAllMetrics(dsInfo)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
|
||||
customMetricsMetricsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute)
|
||||
|
||||
@ -330,11 +329,6 @@ func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*data
|
||||
var dimensionsCacheLock sync.Mutex
|
||||
|
||||
func getDimensionsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
|
||||
result, err := getAllMetrics(dsInfo)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
dimensionsCacheLock.Lock()
|
||||
defer dimensionsCacheLock.Unlock()
|
||||
|
||||
@ -352,6 +346,10 @@ func getDimensionsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*d
|
||||
if customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire.After(time.Now()) {
|
||||
return customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache, nil
|
||||
}
|
||||
result, err := getAllMetrics(dsInfo)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Cache = make([]string, 0)
|
||||
customMetricsDimensionsMap[dsInfo.Profile][dsInfo.Region][dsInfo.Namespace].Expire = time.Now().Add(5 * time.Minute)
|
||||
|
||||
|
@ -147,15 +147,16 @@ func GetPluginDashboards(c *middleware.Context) Response {
|
||||
}
|
||||
}
|
||||
|
||||
func GetPluginReadme(c *middleware.Context) Response {
|
||||
func GetPluginMarkdown(c *middleware.Context) Response {
|
||||
pluginId := c.Params(":pluginId")
|
||||
name := c.Params(":name")
|
||||
|
||||
if content, err := plugins.GetPluginReadme(pluginId); err != nil {
|
||||
if content, err := plugins.GetPluginMarkdown(pluginId, name); err != nil {
|
||||
if notfound, ok := err.(plugins.PluginNotFoundError); ok {
|
||||
return ApiError(404, notfound.Error(), nil)
|
||||
}
|
||||
|
||||
return ApiError(500, "Could not get readme", err)
|
||||
return ApiError(500, "Could not get markdown file", err)
|
||||
} else {
|
||||
return Respond(200, content)
|
||||
}
|
||||
|
@ -3,10 +3,8 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/trace"
|
||||
"strconv"
|
||||
@ -16,7 +14,6 @@ import (
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -87,46 +84,11 @@ func main() {
|
||||
server.Start()
|
||||
}
|
||||
|
||||
func initRuntime() {
|
||||
err := setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
Config: *configFile,
|
||||
HomePath: *homePath,
|
||||
Args: flag.Args(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(3, err.Error())
|
||||
}
|
||||
|
||||
logger := log.New("main")
|
||||
logger.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
|
||||
setting.LogConfigurationInfo()
|
||||
}
|
||||
|
||||
func initSql() {
|
||||
sqlstore.NewEngine()
|
||||
sqlstore.EnsureAdminUser()
|
||||
}
|
||||
|
||||
func writePIDFile() {
|
||||
if *pidFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the required directory structure exists.
|
||||
err := os.MkdirAll(filepath.Dir(*pidFile), 0700)
|
||||
if err != nil {
|
||||
log.Fatal(3, "Failed to verify pid directory", err)
|
||||
}
|
||||
|
||||
// Retrieve the PID and write it.
|
||||
pid := strconv.Itoa(os.Getpid())
|
||||
if err := ioutil.WriteFile(*pidFile, []byte(pid), 0644); err != nil {
|
||||
log.Fatal(3, "Failed to write pidfile", err)
|
||||
}
|
||||
}
|
||||
|
||||
func listenToSystemSignals(server models.GrafanaServer) {
|
||||
signalChan := make(chan os.Signal, 1)
|
||||
ignoreChan := make(chan os.Signal, 1)
|
||||
|
@ -2,7 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
@ -45,8 +50,9 @@ type GrafanaServerImpl struct {
|
||||
func (g *GrafanaServerImpl) Start() {
|
||||
go listenToSystemSignals(g)
|
||||
|
||||
writePIDFile()
|
||||
initRuntime()
|
||||
g.initLogging()
|
||||
g.writePIDFile()
|
||||
|
||||
initSql()
|
||||
metrics.Init()
|
||||
search.Init()
|
||||
@ -74,6 +80,22 @@ func (g *GrafanaServerImpl) Start() {
|
||||
g.startHttpServer()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) initLogging() {
|
||||
err := setting.NewConfigContext(&setting.CommandLineArgs{
|
||||
Config: *configFile,
|
||||
HomePath: *homePath,
|
||||
Args: flag.Args(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
g.log.Error(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
g.log.Info("Starting Grafana", "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
setting.LogConfigurationInfo()
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) startHttpServer() {
|
||||
g.httpServer = api.NewHttpServer()
|
||||
|
||||
@ -101,3 +123,25 @@ func (g *GrafanaServerImpl) Shutdown(code int, reason string) {
|
||||
log.Close()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func (g *GrafanaServerImpl) writePIDFile() {
|
||||
if *pidFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the required directory structure exists.
|
||||
err := os.MkdirAll(filepath.Dir(*pidFile), 0700)
|
||||
if err != nil {
|
||||
g.log.Error("Failed to verify pid directory", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Retrieve the PID and write it.
|
||||
pid := strconv.Itoa(os.Getpid())
|
||||
if err := ioutil.WriteFile(*pidFile, []byte(pid), 0644); err != nil {
|
||||
g.log.Error("Failed to write pidfile", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
g.log.Info("Writing PID file", "path", *pidFile, "pid", pid)
|
||||
}
|
||||
|
@ -1,15 +1,21 @@
|
||||
package plugins
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type DataSourcePlugin struct {
|
||||
FrontendPluginBase
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
BuiltIn bool `json:"builtIn"`
|
||||
Mixed bool `json:"mixed"`
|
||||
Routes []*AppPluginRoute `json:"routes"`
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
Mixed bool `json:"mixed,omitempty"`
|
||||
HasQueryHelp bool `json:"hasQueryHelp,omitempty"`
|
||||
Routes []*AppPluginRoute `json:"routes"`
|
||||
}
|
||||
|
||||
func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
@ -21,6 +27,15 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// look for help markdown
|
||||
helpPath := filepath.Join(p.PluginDir, "QUERY_HELP.md")
|
||||
if _, err := os.Stat(helpPath); os.IsNotExist(err) {
|
||||
helpPath = filepath.Join(p.PluginDir, "query_help.md")
|
||||
}
|
||||
if _, err := os.Stat(helpPath); err == nil {
|
||||
p.HasQueryHelp = true
|
||||
}
|
||||
|
||||
DataSources[p.Id] = p
|
||||
return nil
|
||||
}
|
||||
|
@ -38,8 +38,8 @@ type PluginBase struct {
|
||||
Includes []*PluginInclude `json:"includes"`
|
||||
Module string `json:"module"`
|
||||
BaseUrl string `json:"baseUrl"`
|
||||
HideFromList bool `json:"hideFromList"`
|
||||
State string `json:"state"`
|
||||
HideFromList bool `json:"hideFromList,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
|
||||
IncludedInAppId string `json:"-"`
|
||||
PluginDir string `json:"-"`
|
||||
@ -48,9 +48,6 @@ type PluginBase struct {
|
||||
|
||||
GrafanaNetVersion string `json:"-"`
|
||||
GrafanaNetHasUpdate bool `json:"-"`
|
||||
|
||||
// cache for readme file contents
|
||||
Readme []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (pb *PluginBase) registerPlugin(pluginDir string) error {
|
||||
|
@ -3,6 +3,7 @@ package plugins
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
@ -166,30 +167,24 @@ func (scanner *PluginScanner) loadPluginJson(pluginJsonFilePath string) error {
|
||||
return loader.Load(jsonParser, currentDir)
|
||||
}
|
||||
|
||||
func GetPluginReadme(pluginId string) ([]byte, error) {
|
||||
func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
|
||||
plug, exists := Plugins[pluginId]
|
||||
if !exists {
|
||||
return nil, PluginNotFoundError{pluginId}
|
||||
}
|
||||
|
||||
if plug.Readme != nil {
|
||||
return plug.Readme, nil
|
||||
path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
path = filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToLower(name)))
|
||||
}
|
||||
|
||||
readmePath := filepath.Join(plug.PluginDir, "README.md")
|
||||
if _, err := os.Stat(readmePath); os.IsNotExist(err) {
|
||||
readmePath = filepath.Join(plug.PluginDir, "readme.md")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return make([]byte, 0), nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(readmePath); os.IsNotExist(err) {
|
||||
plug.Readme = make([]byte, 0)
|
||||
return plug.Readme, nil
|
||||
}
|
||||
|
||||
if readmeBytes, err := ioutil.ReadFile(readmePath); err != nil {
|
||||
if data, err := ioutil.ReadFile(path); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
plug.Readme = readmeBytes
|
||||
return plug.Readme, nil
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dasboard</a>"
|
||||
message := evalContext.GetNotificationTitle() + " in state " + evalContext.GetStateModel().Text + "<br><a href=" + ruleUrl + ">Check Dashboard</a>"
|
||||
fields := make([]map[string]interface{}, 0)
|
||||
message += "<br>"
|
||||
for index, evt := range evalContext.EvalMatches {
|
||||
|
@ -114,7 +114,7 @@ func getEngine() (*xorm.Engine, error) {
|
||||
protocol = "unix"
|
||||
}
|
||||
|
||||
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4&allowNativePasswords=true",
|
||||
cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true",
|
||||
DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name)
|
||||
|
||||
if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" {
|
||||
|
@ -12,7 +12,7 @@ type TestDB struct {
|
||||
}
|
||||
|
||||
var TestDB_Sqlite3 = TestDB{DriverName: "sqlite3", ConnStr: ":memory:?_loc=Local"}
|
||||
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?charset=utf8mb4"}
|
||||
var TestDB_Mysql = TestDB{DriverName: "mysql", ConnStr: "grafana:password@tcp(localhost:3306)/grafana_tests?collation=utf8mb4_unicode_ci"}
|
||||
var TestDB_Postgres = TestDB{DriverName: "postgres", ConnStr: "user=grafanatest password=grafanatest host=localhost port=5432 dbname=grafanatest sslmode=disable"}
|
||||
|
||||
func CleanDB(x *xorm.Engine) {
|
||||
|
@ -151,8 +151,12 @@ func (query *Query) renderMeasurement() string {
|
||||
func (query *Query) renderWhereClause() string {
|
||||
res := " WHERE "
|
||||
conditions := query.renderTags()
|
||||
res += strings.Join(conditions, " ")
|
||||
if len(conditions) > 0 {
|
||||
if len(conditions) > 1 {
|
||||
res += "(" + strings.Join(conditions, " ") + ")"
|
||||
} else {
|
||||
res += conditions[0]
|
||||
}
|
||||
res += " AND "
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
|
||||
|
||||
rawQuery, err := query.Build(queryContext)
|
||||
So(err, ShouldBeNil)
|
||||
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE "hostname" = 'server1' OR "hostname" = 'server2' AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
|
||||
So(rawQuery, ShouldEqual, `SELECT mean("value") FROM "cpu" WHERE ("hostname" = 'server1' OR "hostname" = 'server2') AND time > now() - 5m GROUP BY time(5s), "datacenter" fill(null)`)
|
||||
})
|
||||
|
||||
Convey("can build query with math part", func() {
|
||||
|
@ -66,7 +66,7 @@ func (e *MysqlExecutor) initEngine() error {
|
||||
}
|
||||
}
|
||||
|
||||
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?charset=utf8mb4&parseTime=true&loc=UTC", e.datasource.User, e.datasource.Password, "tcp", e.datasource.Url, e.datasource.Database)
|
||||
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC", e.datasource.User, e.datasource.Password, "tcp", e.datasource.Url, e.datasource.Database)
|
||||
e.log.Debug("getEngine", "connection", cnnstr)
|
||||
|
||||
engine, err := xorm.NewEngine("mysql", cnnstr)
|
||||
|
209
public/app/core/components/code_editor/code_editor.ts
Normal file
209
public/app/core/components/code_editor/code_editor.ts
Normal file
@ -0,0 +1,209 @@
|
||||
/**
|
||||
* codeEditor directive based on Ace code editor
|
||||
* https://github.com/ajaxorg/ace
|
||||
*
|
||||
* Basic usage:
|
||||
* <code-editor content="ctrl.target.query" on-change="ctrl.panelCtrl.refresh()"
|
||||
* data-mode="sql" data-show-gutter>
|
||||
* </code-editor>
|
||||
*
|
||||
* Params:
|
||||
* content: Editor content.
|
||||
* onChange: Function called on content change (invoked on editor blur, ctrl+enter, not on every change).
|
||||
* getCompleter: Function returned external completer. Completer is an object implemented getCompletions() method,
|
||||
* see Prometheus Data Source implementation for details.
|
||||
*
|
||||
* Some Ace editor options available via data-* attributes:
|
||||
* data-mode - Language mode (text, sql, javascript, etc.). Default is 'text'.
|
||||
* data-theme - Editor theme (eg 'solarized_dark').
|
||||
* data-max-lines - Max editor height in lines. Editor grows automatically from 1 to maxLines.
|
||||
* data-show-gutter - Show gutter (contains line numbers and additional info).
|
||||
* data-tab-size - Tab size, default is 2.
|
||||
* data-behaviours-enabled - Specifies whether to use behaviors or not. "Behaviors" in this case is the auto-pairing of
|
||||
* special characters, like quotation marks, parenthesis, or brackets.
|
||||
*
|
||||
* Keybindings:
|
||||
* Ctrl-Enter (Command-Enter): run onChange() function
|
||||
*/
|
||||
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import config from 'app/core/config';
|
||||
import ace from 'ace';
|
||||
|
||||
const ACE_SRC_BASE = "public/vendor/npm/ace-builds/src-noconflict/";
|
||||
const DEFAULT_THEME_DARK = "grafana-dark";
|
||||
const DEFAULT_THEME_LIGHT = "textmate";
|
||||
const DEFAULT_MODE = "text";
|
||||
const DEFAULT_MAX_LINES = 10;
|
||||
const DEFAULT_TAB_SIZE = 2;
|
||||
const DEFAULT_BEHAVIOURS = true;
|
||||
|
||||
const GRAFANA_MODULES = ['mode-prometheus', 'snippets-prometheus', 'theme-grafana-dark'];
|
||||
const GRAFANA_MODULE_BASE = "public/app/core/components/code_editor/";
|
||||
|
||||
// Trick for loading additional modules
|
||||
function setModuleUrl(moduleType, name) {
|
||||
let baseUrl = ACE_SRC_BASE;
|
||||
let aceModeName = `ace/${moduleType}/${name}`;
|
||||
let moduleName = `${moduleType}-${name}`;
|
||||
let componentName = `${moduleName}.js`;
|
||||
|
||||
if (_.includes(GRAFANA_MODULES, moduleName)) {
|
||||
baseUrl = GRAFANA_MODULE_BASE;
|
||||
}
|
||||
|
||||
if (moduleType === 'snippets') {
|
||||
componentName = `${moduleType}/${name}.js`;
|
||||
}
|
||||
|
||||
ace.config.setModuleUrl(aceModeName, baseUrl + componentName);
|
||||
}
|
||||
|
||||
setModuleUrl("ext", "language_tools");
|
||||
setModuleUrl("mode", "text");
|
||||
setModuleUrl("snippets", "text");
|
||||
|
||||
let editorTemplate = `<div></div>`;
|
||||
|
||||
function link(scope, elem, attrs) {
|
||||
let lightTheme = config.bootData.user.lightTheme;
|
||||
let default_theme = lightTheme ? DEFAULT_THEME_LIGHT : DEFAULT_THEME_DARK;
|
||||
|
||||
// Options
|
||||
let langMode = attrs.mode || DEFAULT_MODE;
|
||||
let maxLines = attrs.maxLines || DEFAULT_MAX_LINES;
|
||||
let showGutter = attrs.showGutter !== undefined;
|
||||
let theme = attrs.theme || default_theme;
|
||||
let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE;
|
||||
let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS;
|
||||
|
||||
// Initialize editor
|
||||
let aceElem = elem.get(0);
|
||||
let codeEditor = ace.edit(aceElem);
|
||||
let editorSession = codeEditor.getSession();
|
||||
|
||||
let editorOptions = {
|
||||
maxLines: maxLines,
|
||||
showGutter: showGutter,
|
||||
tabSize: tabSize,
|
||||
behavioursEnabled: behavioursEnabled,
|
||||
highlightActiveLine: false,
|
||||
showPrintMargin: false,
|
||||
autoScrollEditorIntoView: true // this is needed if editor is inside scrollable page
|
||||
};
|
||||
|
||||
// Set options
|
||||
codeEditor.setOptions(editorOptions);
|
||||
// disable depreacation warning
|
||||
codeEditor.$blockScrolling = Infinity;
|
||||
// Padding hacks
|
||||
codeEditor.renderer.setScrollMargin(15, 15);
|
||||
codeEditor.renderer.setPadding(10);
|
||||
|
||||
setThemeMode(theme);
|
||||
setLangMode(langMode);
|
||||
setEditorContent(scope.content);
|
||||
|
||||
// Add classes
|
||||
elem.addClass("gf-code-editor");
|
||||
let textarea = elem.find("textarea");
|
||||
textarea.addClass('gf-form-input');
|
||||
|
||||
// Event handlers
|
||||
editorSession.on('change', (e) => {
|
||||
scope.$apply(() => {
|
||||
let newValue = codeEditor.getValue();
|
||||
scope.content = newValue;
|
||||
});
|
||||
});
|
||||
|
||||
// Sync with outer scope - update editor content if model has been changed from outside of directive.
|
||||
scope.$watch('content', (newValue, oldValue) => {
|
||||
let editorValue = codeEditor.getValue();
|
||||
if (newValue !== editorValue && newValue !== oldValue) {
|
||||
scope.$$postDigest(function() {
|
||||
setEditorContent(newValue);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
codeEditor.on('blur', () => {
|
||||
scope.onChange();
|
||||
});
|
||||
|
||||
scope.$on("$destroy", () => {
|
||||
codeEditor.destroy();
|
||||
});
|
||||
|
||||
// Keybindings
|
||||
codeEditor.commands.addCommand({
|
||||
name: 'executeQuery',
|
||||
bindKey: {win: 'Ctrl-Enter', mac: 'Command-Enter'},
|
||||
exec: () => {
|
||||
scope.onChange();
|
||||
}
|
||||
});
|
||||
|
||||
function setLangMode(lang) {
|
||||
let aceModeName = `ace/mode/${lang}`;
|
||||
setModuleUrl("mode", lang);
|
||||
setModuleUrl("snippets", lang);
|
||||
editorSession.setMode(aceModeName);
|
||||
|
||||
ace.config.loadModule("ace/ext/language_tools", (language_tools) => {
|
||||
codeEditor.setOptions({
|
||||
enableBasicAutocompletion: true,
|
||||
enableLiveAutocompletion: true,
|
||||
enableSnippets: true
|
||||
});
|
||||
|
||||
if (scope.getCompleter()) {
|
||||
// make copy of array as ace seems to share completers array between instances
|
||||
codeEditor.completers = codeEditor.completers.slice();
|
||||
codeEditor.completers.push(scope.getCompleter());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setThemeMode(theme) {
|
||||
setModuleUrl("theme", theme);
|
||||
let themeModule = `ace/theme/${theme}`;
|
||||
ace.config.loadModule(themeModule, (theme_module) => {
|
||||
// Check is theme light or dark and fix if needed
|
||||
let lightTheme = config.bootData.user.lightTheme;
|
||||
let fixedTheme = theme;
|
||||
if (lightTheme && theme_module.isDark) {
|
||||
fixedTheme = DEFAULT_THEME_LIGHT;
|
||||
} else if (!lightTheme && !theme_module.isDark) {
|
||||
fixedTheme = DEFAULT_THEME_DARK;
|
||||
}
|
||||
setModuleUrl("theme", fixedTheme);
|
||||
themeModule = `ace/theme/${fixedTheme}`;
|
||||
codeEditor.setTheme(themeModule);
|
||||
|
||||
elem.addClass("gf-code-editor--theme-loaded");
|
||||
});
|
||||
}
|
||||
|
||||
function setEditorContent(value) {
|
||||
codeEditor.setValue(value);
|
||||
codeEditor.clearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
export function codeEditorDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: editorTemplate,
|
||||
scope: {
|
||||
content: "=",
|
||||
onChange: "&",
|
||||
getCompleter: "&"
|
||||
},
|
||||
link: link
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('codeEditor', codeEditorDirective);
|
481
public/app/core/components/code_editor/mode-prometheus.js
Normal file
481
public/app/core/components/code_editor/mode-prometheus.js
Normal file
@ -0,0 +1,481 @@
|
||||
// jshint ignore: start
|
||||
// jscs: disable
|
||||
ace.define("ace/mode/prometheus_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var oop = require("../lib/oop");
|
||||
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
|
||||
|
||||
var PrometheusHighlightRules = function() {
|
||||
var keywords = (
|
||||
"by|without|keep_common|offset|bool|and|or|unless|ignoring|on|group_left|group_right|" +
|
||||
"count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile"
|
||||
);
|
||||
|
||||
var builtinConstants = (
|
||||
"true|false|null|__name__|job"
|
||||
);
|
||||
|
||||
var builtinFunctions = (
|
||||
"abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv|" + "drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2|" +
|
||||
"log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time|" +
|
||||
"min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time"
|
||||
);
|
||||
|
||||
var keywordMapper = this.createKeywordMapper({
|
||||
"support.function": builtinFunctions,
|
||||
"keyword": keywords,
|
||||
"constant.language": builtinConstants
|
||||
}, "identifier", true);
|
||||
|
||||
this.$rules = {
|
||||
"start" : [ {
|
||||
token : "string", // single line
|
||||
regex : /"(?:[^"\\]|\\.)*?"/
|
||||
}, {
|
||||
token : "string", // string
|
||||
regex : "'.*?'"
|
||||
}, {
|
||||
token : "constant.numeric", // float
|
||||
regex : "[-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"
|
||||
}, {
|
||||
token : "constant.language", // time
|
||||
regex : "\\d+[smhdwy]"
|
||||
}, {
|
||||
token : keywordMapper,
|
||||
regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b"
|
||||
}, {
|
||||
token : "keyword.operator",
|
||||
regex : "\\+|\\-|\\*|\\/|%|\\^|=|==|!=|<=|>=|<|>|=\\~|!\\~"
|
||||
}, {
|
||||
token : "paren.lparen",
|
||||
regex : "[[({]"
|
||||
}, {
|
||||
token : "paren.rparen",
|
||||
regex : "[\\])}]"
|
||||
}, {
|
||||
token : "text",
|
||||
regex : "\\s+"
|
||||
} ]
|
||||
};
|
||||
};
|
||||
|
||||
oop.inherits(PrometheusHighlightRules, TextHighlightRules);
|
||||
|
||||
exports.PrometheusHighlightRules = PrometheusHighlightRules;
|
||||
});
|
||||
|
||||
ace.define("ace/mode/prometheus_completions",["require","exports","module","ace/token_iterator", "ace/lib/lang"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var lang = require("../lib/lang");
|
||||
|
||||
var prometheusKeyWords = [
|
||||
"by", "without", "keep_common", "offset", "bool", "and", "or", "unless", "ignoring", "on", "group_left",
|
||||
"group_right", "count", "count_values", "min", "max", "avg", "sum", "stddev", "stdvar", "bottomk", "topk", "quantile"
|
||||
];
|
||||
|
||||
var keyWordsCompletions = prometheusKeyWords.map(function(word) {
|
||||
return {
|
||||
caption: word,
|
||||
value: word,
|
||||
meta: "keyword",
|
||||
score: Number.MAX_VALUE
|
||||
}
|
||||
});
|
||||
|
||||
var prometheusFunctions = [
|
||||
{
|
||||
name: 'abs()', value: 'abs',
|
||||
def: 'abs(v instant-vector)',
|
||||
docText: 'Returns the input vector with all sample values converted to their absolute value.'
|
||||
},
|
||||
{
|
||||
name: 'abs()', value: 'abs',
|
||||
def: 'abs(v instant-vector)',
|
||||
docText: 'Returns the input vector with all sample values converted to their absolute value.'
|
||||
},
|
||||
{
|
||||
name: 'absent()', value: 'absent',
|
||||
def: 'absent(v instant-vector)',
|
||||
docText: 'Returns an empty vector if the vector passed to it has any elements and a 1-element vector with the value 1 if the vector passed to it has no elements. This is useful for alerting on when no time series exist for a given metric name and label combination.'
|
||||
},
|
||||
{
|
||||
name: 'ceil()', value: 'ceil',
|
||||
def: 'ceil(v instant-vector)',
|
||||
docText: 'Rounds the sample values of all elements in `v` up to the nearest integer.'
|
||||
},
|
||||
{
|
||||
name: 'changes()', value: 'changes',
|
||||
def: 'changes(v range-vector)',
|
||||
docText: 'For each input time series, `changes(v range-vector)` returns the number of times its value has changed within the provided time range as an instant vector.'
|
||||
},
|
||||
{
|
||||
name: 'clamp_max()', value: 'clamp_max',
|
||||
def: 'clamp_max(v instant-vector, max scalar)',
|
||||
docText: 'Clamps the sample values of all elements in `v` to have an upper limit of `max`.'
|
||||
},
|
||||
{
|
||||
name: 'clamp_min()', value: 'clamp_min',
|
||||
def: 'clamp_min(v instant-vector, min scalar)',
|
||||
docText: 'Clamps the sample values of all elements in `v` to have a lower limit of `min`.'
|
||||
},
|
||||
{
|
||||
name: 'count_scalar()', value: 'count_scalar',
|
||||
def: 'count_scalar(v instant-vector)',
|
||||
docText: 'Returns the number of elements in a time series vector as a scalar. This is in contrast to the `count()` aggregation operator, which always returns a vector (an empty one if the input vector is empty) and allows grouping by labels via a `by` clause.'
|
||||
},
|
||||
{
|
||||
name: 'day_of_month()', value: 'day_of_month',
|
||||
def: 'day_of_month(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the day of the month for each of the given times in UTC. Returned values are from 1 to 31.'
|
||||
},
|
||||
{
|
||||
name: 'day_of_week()', value: 'day_of_week',
|
||||
def: 'day_of_week(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the day of the week for each of the given times in UTC. Returned values are from 0 to 6, where 0 means Sunday etc.'
|
||||
},
|
||||
{
|
||||
name: 'days_in_month()', value: 'days_in_month',
|
||||
def: 'days_in_month(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns number of days in the month for each of the given times in UTC. Returned values are from 28 to 31.'
|
||||
},
|
||||
{
|
||||
name: 'delta()', value: 'delta',
|
||||
def: 'delta(v range-vector)',
|
||||
docText: 'Calculates the difference between the first and last value of each time series element in a range vector `v`, returning an instant vector with the given deltas and equivalent labels. The delta is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if the sample values are all integers.'
|
||||
},
|
||||
{
|
||||
name: 'deriv()', value: 'deriv',
|
||||
def: 'deriv(v range-vector)',
|
||||
docText: 'Calculates the per-second derivative of the time series in a range vector `v`, using simple linear regression.'
|
||||
},
|
||||
{
|
||||
name: 'drop_common_labels()', value: 'drop_common_labels',
|
||||
def: 'drop_common_labels(instant-vector)',
|
||||
docText: 'Drops all labels that have the same name and value across all series in the input vector.'
|
||||
},
|
||||
{
|
||||
name: 'exp()', value: 'exp',
|
||||
def: 'exp(v instant-vector)',
|
||||
docText: 'Calculates the exponential function for all elements in `v`.\nSpecial cases are:\n* `Exp(+Inf) = +Inf` \n* `Exp(NaN) = NaN`'
|
||||
},
|
||||
{
|
||||
name: 'floor()', value: 'floor',
|
||||
def: 'floor(v instant-vector)',
|
||||
docText: 'Rounds the sample values of all elements in `v` down to the nearest integer.'
|
||||
},
|
||||
{
|
||||
name: 'histogram_quantile()', value: 'histogram_quantile',
|
||||
def: 'histogram_quantile(φ float, b instant-vector)',
|
||||
docText: 'Calculates the φ-quantile (0 ≤ φ ≤ 1) from the buckets `b` of a histogram. The samples in `b` are the counts of observations in each bucket. Each sample must have a label `le` where the label value denotes the inclusive upper bound of the bucket. (Samples without such a label are silently ignored.) The histogram metric type automatically provides time series with the `_bucket` suffix and the appropriate labels.'
|
||||
},
|
||||
{
|
||||
name: 'holt_winters()', value: 'holt_winters',
|
||||
def: 'holt_winters(v range-vector, sf scalar, tf scalar)',
|
||||
docText: 'Produces a smoothed value for time series based on the range in `v`. The lower the smoothing factor `sf`, the more importance is given to old data. The higher the trend factor `tf`, the more trends in the data is considered. Both `sf` and `tf` must be between 0 and 1.'
|
||||
},
|
||||
{
|
||||
name: 'hour()', value: 'hour',
|
||||
def: 'hour(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the hour of the day for each of the given times in UTC. Returned values are from 0 to 23.'
|
||||
},
|
||||
{
|
||||
name: 'idelta()', value: 'idelta',
|
||||
def: 'idelta(v range-vector)',
|
||||
docText: 'Calculates the difference between the last two samples in the range vector `v`, returning an instant vector with the given deltas and equivalent labels.'
|
||||
},
|
||||
{
|
||||
name: 'increase()', value: 'increase',
|
||||
def: 'increase(v range-vector)',
|
||||
docText: 'Calculates the increase in the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. The increase is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if a counter increases only by integer increments.'
|
||||
},
|
||||
{
|
||||
name: 'irate()', value: 'irate',
|
||||
def: 'irate(v range-vector)',
|
||||
docText: 'Calculates the per-second instant rate of increase of the time series in the range vector. This is based on the last two data points. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for.'
|
||||
},
|
||||
{
|
||||
name: 'label_replace()', value: 'label_replace',
|
||||
def: 'label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)',
|
||||
docText: 'For each timeseries in `v`, `label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)` matches the regular expression `regex` against the label `src_label`. If it matches, then the timeseries is returned with the label `dst_label` replaced by the expansion of `replacement`. `$1` is replaced with the first matching subgroup, `$2` with the second etc. If the regular expression doesn\'t match then the timeseries is returned unchanged.'
|
||||
},
|
||||
{
|
||||
name: 'ln()', value: 'ln',
|
||||
def: 'ln(v instant-vector)',
|
||||
docText: 'calculates the natural logarithm for all elements in `v`.\nSpecial cases are:\n * `ln(+Inf) = +Inf`\n * `ln(0) = -Inf`\n * `ln(x < 0) = NaN`\n * `ln(NaN) = NaN`'
|
||||
},
|
||||
{
|
||||
name: 'log2()', value: 'log2',
|
||||
def: 'log2(v instant-vector)',
|
||||
docText: 'Calculates the binary logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.'
|
||||
},
|
||||
{
|
||||
name: 'log10()', value: 'log10',
|
||||
def: 'log10(v instant-vector)',
|
||||
docText: 'Calculates the decimal logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.'
|
||||
},
|
||||
{
|
||||
name: 'minute()', value: 'minute',
|
||||
def: 'minute(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the minute of the hour for each of the given times in UTC. Returned values are from 0 to 59.'
|
||||
},
|
||||
{
|
||||
name: 'month()', value: 'month',
|
||||
def: 'month(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the month of the year for each of the given times in UTC. Returned values are from 1 to 12, where 1 means January etc.'
|
||||
},
|
||||
{
|
||||
name: 'predict_linear()', value: 'predict_linear',
|
||||
def: 'predict_linear(v range-vector, t scalar)',
|
||||
docText: 'Predicts the value of time series `t` seconds from now, based on the range vector `v`, using simple linear regression.'
|
||||
},
|
||||
{
|
||||
name: 'rate()', value: 'rate',
|
||||
def: 'rate(v range-vector)',
|
||||
docText: "Calculates the per-second average rate of increase of the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of scrape cycles with the range's time period."
|
||||
},
|
||||
{
|
||||
name: 'resets()', value: 'resets',
|
||||
def: 'resets(v range-vector)',
|
||||
docText: 'For each input time series, `resets(v range-vector)` returns the number of counter resets within the provided time range as an instant vector. Any decrease in the value between two consecutive samples is interpreted as a counter reset.'
|
||||
},
|
||||
{
|
||||
name: 'round()', value: 'round',
|
||||
def: 'round(v instant-vector, to_nearest=1 scalar)',
|
||||
docText: 'Rounds the sample values of all elements in `v` to the nearest integer. Ties are resolved by rounding up. The optional `to_nearest` argument allows specifying the nearest multiple to which the sample values should be rounded. This multiple may also be a fraction.'
|
||||
},
|
||||
{
|
||||
name: 'scalar()', value: 'scalar',
|
||||
def: 'scalar(v instant-vector)',
|
||||
docText: 'Given a single-element input vector, `scalar(v instant-vector)` returns the sample value of that single element as a scalar. If the input vector does not have exactly one element, `scalar` will return `NaN`.'
|
||||
},
|
||||
{
|
||||
name: 'sort()', value: 'sort',
|
||||
def: 'sort(v instant-vector)',
|
||||
docText: 'Returns vector elements sorted by their sample values, in ascending order.'
|
||||
},
|
||||
{
|
||||
name: 'sort_desc()', value: 'sort_desc',
|
||||
def: 'sort_desc(v instant-vector)',
|
||||
docText: 'Returns vector elements sorted by their sample values, in descending order.'
|
||||
},
|
||||
{
|
||||
name: 'sqrt()', value: 'sqrt',
|
||||
def: 'sqrt(v instant-vector)',
|
||||
docText: 'Calculates the square root of all elements in `v`.'
|
||||
},
|
||||
{
|
||||
name: 'time()', value: 'time',
|
||||
def: 'time()',
|
||||
docText: 'Returns the number of seconds since January 1, 1970 UTC. Note that this does not actually return the current time, but the time at which the expression is to be evaluated.'
|
||||
},
|
||||
{
|
||||
name: 'vector()', value: 'vector',
|
||||
def: 'vector(s scalar)',
|
||||
docText: 'Returns the scalar `s` as a vector with no labels.'
|
||||
},
|
||||
{
|
||||
name: 'year()', value: 'year',
|
||||
def: 'year(v=vector(time()) instant-vector)',
|
||||
docText: 'Returns the year for each of the given times in UTC.'
|
||||
},
|
||||
{
|
||||
name: 'avg_over_time()', value: 'avg_over_time',
|
||||
def: 'avg_over_time(range-vector)',
|
||||
docText: 'The average value of all points in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'min_over_time()', value: 'min_over_time',
|
||||
def: 'min_over_time(range-vector)',
|
||||
docText: 'The minimum value of all points in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'max_over_time()', value: 'max_over_time',
|
||||
def: 'max_over_time(range-vector)',
|
||||
docText: 'The maximum value of all points in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'sum_over_time()', value: 'sum_over_time',
|
||||
def: 'sum_over_time(range-vector)',
|
||||
docText: 'The sum of all values in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'count_over_time()', value: 'count_over_time',
|
||||
def: 'count_over_time(range-vector)',
|
||||
docText: 'The count of all values in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'quantile_over_time()', value: 'quantile_over_time',
|
||||
def: 'quantile_over_time(scalar, range-vector)',
|
||||
docText: 'The φ-quantile (0 ≤ φ ≤ 1) of the values in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'stddev_over_time()', value: 'stddev_over_time',
|
||||
def: 'stddev_over_time(range-vector)',
|
||||
docText: 'The population standard deviation of the values in the specified interval.'
|
||||
},
|
||||
{
|
||||
name: 'stdvar_over_time()', value: 'stdvar_over_time',
|
||||
def: 'stdvar_over_time(range-vector)',
|
||||
docText: 'The population standard variance of the values in the specified interval.'
|
||||
},
|
||||
];
|
||||
|
||||
function wrapText(str, len) {
|
||||
len = len || 60;
|
||||
var lines = [];
|
||||
var space_index = 0;
|
||||
var line_start = 0;
|
||||
var next_line_end = len;
|
||||
var line = "";
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
if (str[i] === ' ') {
|
||||
space_index = i;
|
||||
} else if (i >= next_line_end && space_index != 0) {
|
||||
line = str.slice(line_start, space_index);
|
||||
lines.push(line);
|
||||
line_start = space_index + 1;
|
||||
next_line_end = i + len;
|
||||
space_index = 0;
|
||||
}
|
||||
}
|
||||
line = str.slice(line_start);
|
||||
lines.push(line);
|
||||
return lines.join(" <br>");
|
||||
}
|
||||
|
||||
function convertMarkDownTags(text) {
|
||||
text = text.replace(/```(.+)```/, "<pre>$1</pre>");
|
||||
text = text.replace(/`([^`]+)`/, "<code>$1</code>");
|
||||
return text;
|
||||
}
|
||||
|
||||
function convertToHTML(item) {
|
||||
var docText = lang.escapeHTML(item.docText);
|
||||
docText = convertMarkDownTags(wrapText(docText, 40));
|
||||
return [
|
||||
"<b>", lang.escapeHTML(item.def), "</b>", "<hr></hr>", docText, "<br> "
|
||||
].join("");
|
||||
}
|
||||
|
||||
var functionsCompletions = prometheusFunctions.map(function(item) {
|
||||
return {
|
||||
caption: item.name,
|
||||
value: item.value,
|
||||
docHTML: convertToHTML(item),
|
||||
meta: "function",
|
||||
score: Number.MAX_VALUE
|
||||
};
|
||||
});
|
||||
|
||||
var PrometheusCompletions = function() {};
|
||||
|
||||
(function() {
|
||||
this.getCompletions = function(state, session, pos, prefix, callback) {
|
||||
var completions = keyWordsCompletions.concat(functionsCompletions);
|
||||
callback(null, completions);
|
||||
};
|
||||
|
||||
}).call(PrometheusCompletions.prototype);
|
||||
|
||||
exports.PrometheusCompletions = PrometheusCompletions;
|
||||
});
|
||||
|
||||
ace.define("ace/mode/behaviour/prometheus",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/mode/behaviour/cstyle","ace/token_iterator"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var oop = require("../../lib/oop");
|
||||
var Behaviour = require("../behaviour").Behaviour;
|
||||
var CstyleBehaviour = require("./cstyle").CstyleBehaviour;
|
||||
var TokenIterator = require("../../token_iterator").TokenIterator;
|
||||
|
||||
function getWrapped(selection, selected, opening, closing) {
|
||||
var rowDiff = selection.end.row - selection.start.row;
|
||||
return {
|
||||
text: opening + selected + closing,
|
||||
selection: [
|
||||
0,
|
||||
selection.start.column + 1,
|
||||
rowDiff,
|
||||
selection.end.column + (rowDiff ? 0 : 1)
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
var PrometheusBehaviour = function () {
|
||||
this.inherit(CstyleBehaviour);
|
||||
|
||||
// Rewrite default CstyleBehaviour for {} braces
|
||||
this.add("braces", "insertion", function(state, action, editor, session, text) {
|
||||
if (text == '{') {
|
||||
var selection = editor.getSelectionRange();
|
||||
var selected = session.doc.getTextRange(selection);
|
||||
if (selected !== "" && editor.getWrapBehavioursEnabled()) {
|
||||
return getWrapped(selection, selected, '{', '}');
|
||||
} else if (CstyleBehaviour.isSaneInsertion(editor, session)) {
|
||||
return {
|
||||
text: '{}',
|
||||
selection: [1, 1]
|
||||
};
|
||||
}
|
||||
} else if (text == '}') {
|
||||
var cursor = editor.getCursorPosition();
|
||||
var line = session.doc.getLine(cursor.row);
|
||||
var rightChar = line.substring(cursor.column, cursor.column + 1);
|
||||
if (rightChar == '}') {
|
||||
var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row});
|
||||
if (matching !== null && CstyleBehaviour.isAutoInsertedClosing(cursor, line, text)) {
|
||||
return {
|
||||
text: '',
|
||||
selection: [1, 1]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.add("braces", "deletion", function(state, action, editor, session, range) {
|
||||
var selected = session.doc.getTextRange(range);
|
||||
if (!range.isMultiLine() && selected == '{') {
|
||||
var line = session.doc.getLine(range.start.row);
|
||||
var rightChar = line.substring(range.start.column + 1, range.start.column + 2);
|
||||
if (rightChar == '}') {
|
||||
range.end.column++;
|
||||
return range;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
oop.inherits(PrometheusBehaviour, CstyleBehaviour);
|
||||
|
||||
exports.PrometheusBehaviour = PrometheusBehaviour;
|
||||
});
|
||||
|
||||
ace.define("ace/mode/prometheus",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/prometheus_highlight_rules"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
var oop = require("../lib/oop");
|
||||
var TextMode = require("./text").Mode;
|
||||
var PrometheusHighlightRules = require("./prometheus_highlight_rules").PrometheusHighlightRules;
|
||||
var PrometheusCompletions = require("./prometheus_completions").PrometheusCompletions;
|
||||
var PrometheusBehaviour = require("./behaviour/prometheus").PrometheusBehaviour;
|
||||
|
||||
var Mode = function() {
|
||||
this.HighlightRules = PrometheusHighlightRules;
|
||||
this.$behaviour = new PrometheusBehaviour();
|
||||
this.$completer = new PrometheusCompletions();
|
||||
// replace keyWordCompleter
|
||||
this.completer = this.$completer;
|
||||
};
|
||||
oop.inherits(Mode, TextMode);
|
||||
|
||||
(function() {
|
||||
|
||||
this.$id = "ace/mode/prometheus";
|
||||
}).call(Mode.prototype);
|
||||
|
||||
exports.Mode = Mode;
|
||||
|
||||
});
|
@ -0,0 +1,21 @@
|
||||
// jshint ignore: start
|
||||
// jscs: disable
|
||||
ace.define("ace/snippets/prometheus",["require","exports","module"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
// exports.snippetText = "# rate\n\
|
||||
// snippet r\n\
|
||||
// rate(${1:metric}[${2:range}])\n\
|
||||
// ";
|
||||
|
||||
exports.snippets = [
|
||||
{
|
||||
"content": "rate(${1:metric}[${2:range}])",
|
||||
"name": "rate()",
|
||||
"scope": "prometheus",
|
||||
"tabTrigger": "r"
|
||||
}
|
||||
];
|
||||
|
||||
exports.scope = "prometheus";
|
||||
});
|
116
public/app/core/components/code_editor/theme-grafana-dark.js
Normal file
116
public/app/core/components/code_editor/theme-grafana-dark.js
Normal file
@ -0,0 +1,116 @@
|
||||
/* jshint ignore:start */
|
||||
|
||||
ace.define("ace/theme/grafana-dark",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
|
||||
"use strict";
|
||||
|
||||
exports.isDark = true;
|
||||
exports.cssClass = "gf-code-dark";
|
||||
exports.cssText = ".gf-code-dark .ace_gutter {\
|
||||
background: #2f3129;\
|
||||
color: #8f908a\
|
||||
}\
|
||||
.gf-code-dark .ace_print-margin {\
|
||||
width: 1px;\
|
||||
background: #555651\
|
||||
}\
|
||||
.gf-code-dark {\
|
||||
background-color: #111;\
|
||||
color: #e0e0e0\
|
||||
}\
|
||||
.gf-code-dark .ace_cursor {\
|
||||
color: #f8f8f0\
|
||||
}\
|
||||
.gf-code-dark .ace_marker-layer .ace_selection {\
|
||||
background: #49483e\
|
||||
}\
|
||||
.gf-code-dark.ace_multiselect .ace_selection.ace_start {\
|
||||
box-shadow: 0 0 3px 0px #272822;\
|
||||
}\
|
||||
.gf-code-dark .ace_marker-layer .ace_step {\
|
||||
background: rgb(102, 82, 0)\
|
||||
}\
|
||||
.gf-code-dark .ace_marker-layer .ace_bracket {\
|
||||
margin: -1px 0 0 -1px;\
|
||||
border: 1px solid #49483e\
|
||||
}\
|
||||
.gf-code-dark .ace_marker-layer .ace_active-line {\
|
||||
background: #202020\
|
||||
}\
|
||||
.gf-code-dark .ace_gutter-active-line {\
|
||||
background-color: #272727\
|
||||
}\
|
||||
.gf-code-dark .ace_marker-layer .ace_selected-word {\
|
||||
border: 1px solid #49483e\
|
||||
}\
|
||||
.gf-code-dark .ace_invisible {\
|
||||
color: #52524d\
|
||||
}\
|
||||
.gf-code-dark .ace_entity.ace_name.ace_tag,\
|
||||
.gf-code-dark .ace_keyword,\
|
||||
.gf-code-dark .ace_meta.ace_tag,\
|
||||
.gf-code-dark .ace_storage {\
|
||||
color: #66d9ef\
|
||||
}\
|
||||
.gf-code-dark .ace_punctuation,\
|
||||
.gf-code-dark .ace_punctuation.ace_tag {\
|
||||
color: #fff\
|
||||
}\
|
||||
.gf-code-dark .ace_constant.ace_character,\
|
||||
.gf-code-dark .ace_constant.ace_language,\
|
||||
.gf-code-dark .ace_constant.ace_numeric,\
|
||||
.gf-code-dark .ace_constant.ace_other {\
|
||||
color: #fe85fc\
|
||||
}\
|
||||
.gf-code-dark .ace_invalid {\
|
||||
color: #f8f8f0;\
|
||||
background-color: #f92672\
|
||||
}\
|
||||
.gf-code-dark .ace_invalid.ace_deprecated {\
|
||||
color: #f8f8f0;\
|
||||
background-color: #ae81ff\
|
||||
}\
|
||||
.gf-code-dark .ace_support.ace_constant,\
|
||||
.gf-code-dark .ace_support.ace_function {\
|
||||
color: #59e6e3\
|
||||
}\
|
||||
.gf-code-dark .ace_fold {\
|
||||
background-color: #a6e22e;\
|
||||
border-color: #f8f8f2\
|
||||
}\
|
||||
.gf-code-dark .ace_storage.ace_type,\
|
||||
.gf-code-dark .ace_support.ace_class,\
|
||||
.gf-code-dark .ace_support.ace_type {\
|
||||
font-style: italic;\
|
||||
color: #66d9ef\
|
||||
}\
|
||||
.gf-code-dark .ace_entity.ace_name.ace_function,\
|
||||
.gf-code-dark .ace_entity.ace_other,\
|
||||
.gf-code-dark .ace_entity.ace_other.ace_attribute-name,\
|
||||
.gf-code-dark .ace_variable {\
|
||||
color: #a6e22e\
|
||||
}\
|
||||
.gf-code-dark .ace_variable.ace_parameter {\
|
||||
font-style: italic;\
|
||||
color: #fd971f\
|
||||
}\
|
||||
.gf-code-dark .ace_string {\
|
||||
color: #74e680\
|
||||
}\
|
||||
.gf-code-dark .ace_paren {\
|
||||
color: #f0a842\
|
||||
}\
|
||||
.gf-code-dark .ace_operator {\
|
||||
color: #FFF\
|
||||
}\
|
||||
.gf-code-dark .ace_comment {\
|
||||
color: #75715e\
|
||||
}\
|
||||
.gf-code-dark .ace_indent-guide {\
|
||||
background: url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaaaccayaaaczgbynaaaaekleqvqimwpq0fd0zxbzd/wpaajvaoxesgneaaaaaelftksuqmcc) right repeat-y\
|
||||
}";
|
||||
|
||||
var dom = require("../lib/dom");
|
||||
dom.importCssString(exports.cssText, exports.cssClass);
|
||||
});
|
||||
|
||||
/* jshint ignore:end */
|
@ -19,6 +19,8 @@ import "./directives/diff-view";
|
||||
import './jquery_extended';
|
||||
import './partials';
|
||||
import './components/jsontree/jsontree';
|
||||
import './components/code_editor/code_editor';
|
||||
import './utils/outline';
|
||||
|
||||
import {grafanaAppDirective} from './components/grafana_app';
|
||||
import {sideMenuDirective} from './components/sidemenu/sidemenu';
|
||||
|
@ -163,21 +163,15 @@ function($, _) {
|
||||
ms: 0.001
|
||||
};
|
||||
|
||||
kbn.calculateInterval = function(range, resolution, userInterval) {
|
||||
kbn.calculateInterval = function(range, resolution, lowLimitInterval) {
|
||||
var lowLimitMs = 1; // 1 millisecond default low limit
|
||||
var intervalMs, lowLimitInterval;
|
||||
var intervalMs;
|
||||
|
||||
if (userInterval) {
|
||||
if (userInterval[0] === '>') {
|
||||
lowLimitInterval = userInterval.slice(1);
|
||||
lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
|
||||
}
|
||||
else {
|
||||
return {
|
||||
intervalMs: kbn.interval_to_ms(userInterval),
|
||||
interval: userInterval,
|
||||
};
|
||||
if (lowLimitInterval) {
|
||||
if (lowLimitInterval[0] === '>') {
|
||||
lowLimitInterval = lowLimitInterval.slice(1);
|
||||
}
|
||||
lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
|
||||
}
|
||||
|
||||
intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution);
|
||||
|
32
public/app/core/utils/outline.js
Normal file
32
public/app/core/utils/outline.js
Normal file
@ -0,0 +1,32 @@
|
||||
// outline.js
|
||||
// based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/
|
||||
(function(d) {
|
||||
"use strict";
|
||||
|
||||
var style_element = d.createElement('STYLE'),
|
||||
dom_events = 'addEventListener' in d,
|
||||
add_event_listener = function(type, callback) {
|
||||
// Basic cross-browser event handling
|
||||
if(dom_events){
|
||||
d.addEventListener(type, callback);
|
||||
} else {
|
||||
d.attachEvent('on' + type, callback);
|
||||
}
|
||||
},
|
||||
set_css = function(css_text) {
|
||||
// Handle setting of <style> element contents in IE8
|
||||
!!style_element.styleSheet ? style_element.styleSheet.cssText = css_text : style_element.innerHTML = css_text;
|
||||
};
|
||||
|
||||
d.getElementsByTagName('HEAD')[0].appendChild(style_element);
|
||||
|
||||
// Using mousedown instead of mouseover, so that previously focused elements don't lose focus ring on mouse move
|
||||
add_event_listener('mousedown', function() {
|
||||
set_css(':focus{outline:0 !important}::-moz-focus-inner{border:0;}');
|
||||
});
|
||||
|
||||
add_event_listener('keydown', function() {
|
||||
set_css('');
|
||||
});
|
||||
|
||||
})(document);
|
@ -12,13 +12,14 @@ import {DashboardExporter} from './exporter';
|
||||
export class DashExportCtrl {
|
||||
dash: any;
|
||||
exporter: DashboardExporter;
|
||||
dismiss: () => void;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) {
|
||||
constructor(private backendSrv, private dashboardSrv, datasourceSrv, private $scope) {
|
||||
this.exporter = new DashboardExporter(datasourceSrv);
|
||||
|
||||
this.exporter.makeExportable(dashboardSrv.getCurrent()).then(dash => {
|
||||
$scope.$apply(() => {
|
||||
this.exporter.makeExportable(this.dashboardSrv.getCurrent()).then(dash => {
|
||||
this.$scope.$apply(() => {
|
||||
this.dash = dash;
|
||||
});
|
||||
});
|
||||
@ -31,11 +32,13 @@ export class DashExportCtrl {
|
||||
}
|
||||
|
||||
saveJson() {
|
||||
var html = angular.toJson(this.dash, true);
|
||||
var uri = "data:application/json," + encodeURIComponent(html);
|
||||
var newWindow = window.open(uri);
|
||||
}
|
||||
var clone = this.dashboardSrv.getCurrent().getSaveModelClone();
|
||||
|
||||
this.$scope.$root.appEvent('show-json-editor', {
|
||||
object: clone,
|
||||
});
|
||||
this.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
export function dashExportDirective() {
|
||||
@ -45,6 +48,7 @@ export function dashExportDirective() {
|
||||
controller: DashExportCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {dismiss: "&"}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareExport.html">
|
||||
<dash-export-modal></dash-export-modal>
|
||||
<dash-export-modal dismiss="dismiss()"></dash-export-modal>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareLinkOptions.html">
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import _ from 'lodash';
|
||||
import {DashboardModel} from '../dashboard/model';
|
||||
import Remarkable from 'remarkable';
|
||||
|
||||
export class MetricsTabCtrl {
|
||||
dsName: string;
|
||||
@ -13,9 +14,15 @@ export class MetricsTabCtrl {
|
||||
dashboard: DashboardModel;
|
||||
panelDsValue: any;
|
||||
addQueryDropdown: any;
|
||||
queryTroubleshooterOpen: boolean;
|
||||
helpOpen: boolean;
|
||||
optionsOpen: boolean;
|
||||
hasQueryHelp: boolean;
|
||||
helpHtml: string;
|
||||
queryOptions: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, private uiSegmentSrv, private datasourceSrv) {
|
||||
constructor($scope, private $sce, private datasourceSrv, private backendSrv, private $timeout) {
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
$scope.ctrl = this;
|
||||
|
||||
@ -33,6 +40,12 @@ export class MetricsTabCtrl {
|
||||
this.addQueryDropdown = {text: 'Add Query', value: null, fake: true};
|
||||
// update next ref id
|
||||
this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
|
||||
this.updateDatasourceOptions();
|
||||
}
|
||||
|
||||
updateDatasourceOptions() {
|
||||
this.hasQueryHelp = this.current.meta.hasQueryHelp;
|
||||
this.queryOptions = this.current.meta.queryOptions;
|
||||
}
|
||||
|
||||
getOptions(includeBuiltin) {
|
||||
@ -50,6 +63,7 @@ export class MetricsTabCtrl {
|
||||
|
||||
this.current = option.datasource;
|
||||
this.panelCtrl.setDatasource(option.datasource);
|
||||
this.updateDatasourceOptions();
|
||||
}
|
||||
|
||||
addMixedQuery(option) {
|
||||
@ -65,6 +79,29 @@ export class MetricsTabCtrl {
|
||||
addQuery() {
|
||||
this.panelCtrl.addQuery({isNew: true});
|
||||
}
|
||||
|
||||
toggleHelp() {
|
||||
this.optionsOpen = false;
|
||||
this.queryTroubleshooterOpen = false;
|
||||
this.helpOpen = !this.helpOpen;
|
||||
|
||||
this.backendSrv.get(`/api/plugins/${this.current.meta.id}/markdown/query_help`).then(res => {
|
||||
var md = new Remarkable();
|
||||
this.helpHtml = this.$sce.trustAsHtml(md.render(res));
|
||||
});
|
||||
}
|
||||
|
||||
toggleOptions() {
|
||||
this.helpOpen = false;
|
||||
this.queryTroubleshooterOpen = false;
|
||||
this.optionsOpen = !this.optionsOpen;
|
||||
}
|
||||
|
||||
toggleQueryTroubleshooter() {
|
||||
this.helpOpen = false;
|
||||
this.optionsOpen = false;
|
||||
this.queryTroubleshooterOpen = !this.queryTroubleshooterOpen;
|
||||
}
|
||||
}
|
||||
|
||||
/** @ngInject **/
|
||||
|
@ -29,7 +29,6 @@ export class PanelCtrl {
|
||||
fullscreen: boolean;
|
||||
inspector: any;
|
||||
editModeInitiated: boolean;
|
||||
editorHelpIndex: number;
|
||||
editMode: any;
|
||||
height: any;
|
||||
containerHeight: any;
|
||||
@ -186,14 +185,6 @@ export class PanelCtrl {
|
||||
this.events.emit('render', payload);
|
||||
}
|
||||
|
||||
toggleEditorHelp(index) {
|
||||
if (this.editorHelpIndex === index) {
|
||||
this.editorHelpIndex = null;
|
||||
return;
|
||||
}
|
||||
this.editorHelpIndex = index;
|
||||
}
|
||||
|
||||
duplicate() {
|
||||
this.dashboard.duplicatePanel(this.panel, this.row);
|
||||
this.$timeout(() => {
|
||||
|
@ -1,9 +1,84 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label gf-query-ds-label">
|
||||
<i class="icon-gf icon-gf-datasources"></i>
|
||||
</label>
|
||||
<label class="gf-form-label">Data Source</label>
|
||||
|
||||
<gf-form-dropdown model="ctrl.panelDsValue" css-class="gf-size-auto"
|
||||
lookup-text="true"
|
||||
get-options="ctrl.getOptions(true)"
|
||||
on-change="ctrl.datasourceChanged($option)">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label gf-form-label--grow"></label>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.queryOptions">
|
||||
<a class="gf-form-label" ng-click="ctrl.toggleOptions()">
|
||||
<i class="fa fa-fw fa-caret-right" ng-hide="ctrl.optionsOpen"></i><i class="fa fa-fw fa-caret-down" ng-show="ctrl.optionsOpen"></i>Options
|
||||
</a>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.hasQueryHelp">
|
||||
<button class="gf-form-label" ng-click="ctrl.toggleHelp()">
|
||||
<i class="fa fa-fw fa-caret-right" ng-hide="ctrl.helpOpen"></i><i class="fa fa-fw fa-caret-down" ng-show="ctrl.helpOpen"></i>Help
|
||||
</button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="gf-form-label" ng-click="ctrl.toggleQueryTroubleshooter()" bs-tooltip="'Display query request & response'">
|
||||
<i class="fa fa-fw fa-caret-right" ng-hide="ctrl.queryTroubleshooterOpen"></i><i class="fa fa-fw fa-caret-down" ng-show="ctrl.queryTroubleshooterOpen"></i>Query Inspector
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div ng-if="ctrl.optionsOpen">
|
||||
<div class="gf-form gf-form--flex-end" ng-if="ctrl.queryOptions.minInterval">
|
||||
<label class="gf-form-label">Min time interval</label>
|
||||
<input type="text" class="gf-form-input width-6" placeholder="{{ctrl.panelCtrl.interval}}" ng-model="ctrl.panel.interval" spellcheck="false" ng-model-onblur ng-change="ctrl.panelCtrl.refresh()" />
|
||||
<info-popover mode="right-absolute">
|
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
|
||||
for example <code>1m</code> if your data is written every minute. Access auto interval via variable <code>$__interval</code> for time range
|
||||
string and <code>$__interval_ms</code> for numeric variable that can be used in math expressions.
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form gf-form--flex-end" ng-if="ctrl.queryOptions.cacheTimeout">
|
||||
<label class="gf-form-label width-9">Cache timeout</label>
|
||||
<input type="text" class="gf-form-input width-6" placeholder="60" ng-model="ctrl.panel.cacheTimeout" ng-model-onblur ng-change="ctrl.panelCtrl.refresh()" spellcheck="false" />
|
||||
<info-popover mode="right-absolute">
|
||||
If your time series store has a query cache this option can override the default
|
||||
cache timeout. Specify a numeric value in seconds.
|
||||
</info-popover>
|
||||
</div>
|
||||
<div class="gf-form gf-form--flex-end" ng-if="ctrl.queryOptions.maxDataPoints">
|
||||
<label class="gf-form-label width-9">Max data points</label>
|
||||
<input type="text" class="gf-form-input width-6" placeholder="auto" ng-model-onblur ng-change="ctrl.panelCtrl.refresh()" ng-model="ctrl.panel.maxDataPoints" spellcheck="false" />
|
||||
<info-popover mode="right-absolute">
|
||||
The maximum data points the query should return. For graphs this
|
||||
is automatically set to one data point per pixel.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box" ng-if="ctrl.helpOpen">
|
||||
<div class="markdown-html" ng-bind-html="ctrl.helpHtml"></div>
|
||||
<a class="grafana-info-box__close" ng-click="ctrl.toggleHelp()">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<query-troubleshooter panel-ctrl="ctrl.panelCtrl" is-open="ctrl.queryTroubleshooterOpen"></query-troubleshooter>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="query-editor-rows gf-form-group">
|
||||
<div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
|
||||
<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
|
||||
<plugin-component type="query-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
<div ng-repeat="target in ctrl.panel.targets" ng-class="{'gf-form-disabled': target.hide}">
|
||||
<rebuild-on-change property="ctrl.panel.datasource || target.datasource" show-null="true">
|
||||
<plugin-component type="query-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-query">
|
||||
@ -14,37 +89,14 @@
|
||||
</span>
|
||||
<span class="gf-form-query-letter-cell-letter">{{ctrl.panelCtrl.nextRefId}}</span>
|
||||
</label>
|
||||
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.current.meta.mixed">
|
||||
Add Query
|
||||
</button>
|
||||
<button class="btn btn-secondary gf-form-btn" ng-click="ctrl.addQuery()" ng-hide="ctrl.current.meta.mixed">
|
||||
Add Query
|
||||
</button>
|
||||
|
||||
<div class="dropdown" ng-if="ctrl.current.meta.mixed">
|
||||
<gf-form-dropdown model="ctrl.addQueryDropdown"
|
||||
get-options="ctrl.getOptions(false)"
|
||||
on-change="ctrl.addMixedQuery($option)">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <query-troubleshooter panel-ctrl="ctrl.panelCtrl"></query-troubleshooter> -->
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">Panel Data Source</label>
|
||||
<gf-form-dropdown model="ctrl.panelDsValue"
|
||||
lookup-text="true"
|
||||
get-options="ctrl.getOptions(true)"
|
||||
on-change="ctrl.datasourceChanged($option)">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<rebuild-on-change property="ctrl.panel.datasource" show-null="true">
|
||||
<plugin-component type="query-options-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
<div class="dropdown" ng-if="ctrl.current.meta.mixed">
|
||||
<gf-form-dropdown model="ctrl.addQueryDropdown" get-options="ctrl.getOptions(false)" on-change="ctrl.addMixedQuery($option)">
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,9 +5,8 @@ import appEvents from 'app/core/app_events';
|
||||
import {coreModule, JsonExplorer} from 'app/core/core';
|
||||
|
||||
const template = `
|
||||
<collapse-box title="Query Troubleshooter" is-open="ctrl.isOpen" state-changed="ctrl.stateChanged()"
|
||||
ng-class="{'collapse-box--error': ctrl.hasError}">
|
||||
<collapse-box-actions>
|
||||
<div class="query-troubleshooter" ng-if="ctrl.isOpen">
|
||||
<div class="query-troubleshooter__header">
|
||||
<a class="pointer" ng-click="ctrl.toggleExpand()" ng-hide="ctrl.allNodesExpanded">
|
||||
<i class="fa fa-plus-square-o"></i> Expand All
|
||||
</a>
|
||||
@ -15,12 +14,12 @@ const template = `
|
||||
<i class="fa fa-minus-square-o"></i> Collapse All
|
||||
</a>
|
||||
<a class="pointer" clipboard-button="ctrl.getClipboardText()"><i class="fa fa-clipboard"></i> Copy to Clipboard</a>
|
||||
</collapse-box-actions>
|
||||
<collapse-box-body>
|
||||
</div>
|
||||
<div class="query-troubleshooter__body">
|
||||
<i class="fa fa-spinner fa-spin" ng-show="ctrl.isLoading"></i>
|
||||
<div class="query-troubleshooter-json"></div>
|
||||
</collapse-box-body>
|
||||
</collapse-box>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export class QueryTroubleshooterCtrl {
|
||||
@ -42,7 +41,9 @@ export class QueryTroubleshooterCtrl {
|
||||
|
||||
appEvents.on('ds-request-response', this.onRequestResponseEventListener);
|
||||
appEvents.on('ds-request-error', this.onRequestErrorEventListener);
|
||||
|
||||
$scope.$on('$destroy', this.removeEventsListeners.bind(this));
|
||||
$scope.$watch('ctrl.isOpen', this.stateChanged.bind(this));
|
||||
}
|
||||
|
||||
removeEventsListeners() {
|
||||
@ -51,6 +52,11 @@ export class QueryTroubleshooterCtrl {
|
||||
}
|
||||
|
||||
onRequestError(err) {
|
||||
// ignore if closed
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isOpen = true;
|
||||
this.hasError = true;
|
||||
this.onRequestResponse(err);
|
||||
@ -133,7 +139,8 @@ export function queryTroubleshooter() {
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
panelCtrl: "="
|
||||
panelCtrl: "=",
|
||||
isOpen: "=",
|
||||
},
|
||||
link: function(scope, elem, attrs, ctrl) {
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import Remarkable from 'remarkable';
|
||||
|
||||
export class PluginEditCtrl {
|
||||
model: any;
|
||||
@ -67,11 +68,9 @@ export class PluginEditCtrl {
|
||||
}
|
||||
|
||||
initReadme() {
|
||||
return this.backendSrv.get(`/api/plugins/${this.pluginId}/readme`).then(res => {
|
||||
return System.import('remarkable').then(Remarkable => {
|
||||
var md = new Remarkable();
|
||||
this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
|
||||
});
|
||||
return this.backendSrv.get(`/api/plugins/${this.pluginId}/markdown/readme`).then(res => {
|
||||
var md = new Remarkable();
|
||||
this.readmeHtml = this.$sce.trustAsHtml(md.render(res));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ export class IntervalVariable implements Variable {
|
||||
this.options.unshift({ text: 'auto', value: '$__auto_interval' });
|
||||
}
|
||||
|
||||
var res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, (this.auto_min ? ">"+this.auto_min : null));
|
||||
var res = kbn.calculateInterval(this.timeSrv.timeRange(), this.auto_count, this.auto_min);
|
||||
this.templateSrv.setGrafanaVariable('$__auto_interval', res.interval);
|
||||
}
|
||||
|
||||
|
5
public/app/headers/common.d.ts
vendored
5
public/app/headers/common.d.ts
vendored
@ -72,3 +72,8 @@ declare module 'd3' {
|
||||
var d3: any;
|
||||
export default d3;
|
||||
}
|
||||
|
||||
declare module 'ace' {
|
||||
var ace: any;
|
||||
export default ace;
|
||||
}
|
||||
|
@ -25,13 +25,14 @@
|
||||
<span class="gf-form-label width-9">Version</span>
|
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.current.jsonData.esVersion" ng-options="f.value as f.name for f in ctrl.esVersions"></select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h3 class="page-heading">Default query settings</h3>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Group by time interval</span>
|
||||
<input class="gf-form-input max-width-9" type="text" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="example: >10s">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Min interval</span>
|
||||
<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="10s"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
|
||||
for example <code>1m</code> if your data is written every minute.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,38 +0,0 @@
|
||||
<section class="grafana-metric-options">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</span>
|
||||
<span class="gf-form-label">Group by time interval</span>
|
||||
|
||||
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.panelCtrl.panel.interval" ng-blur="ctrl.panelCtrl.refresh();"
|
||||
spellcheck='false' placeholder="example: >10s">
|
||||
|
||||
<span class="gf-form-label">
|
||||
<i class="fa fa-question-circle" bs-tooltip="'Set a low limit by having a greater sign: example: >60s'" data-placement="right"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
</span>
|
||||
<span class="gf-form-label width-23">
|
||||
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
alias patterns
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="pull-left">
|
||||
<div class="grafana-info-box" style="border: 0;" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
|
||||
<h5>Alias patterns</h5>
|
||||
<ul ng-non-bindable>
|
||||
<li>{{term fieldname}} = replaced with value of term group by</li>
|
||||
<li>{{metric}} = replaced with metric name (ex. Average, Min, Max)</li>
|
||||
<li>{{field}} = replaced with the metric field name</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
@ -21,5 +21,9 @@
|
||||
},
|
||||
|
||||
"annotations": true,
|
||||
"metrics": true
|
||||
"metrics": true,
|
||||
|
||||
"queryOptions": {
|
||||
"minInterval": true
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
define([
|
||||
'./query_def',
|
||||
'lodash',
|
||||
],
|
||||
function (queryDef, _) {
|
||||
function (queryDef) {
|
||||
'use strict';
|
||||
|
||||
function ElasticQueryBuilder(options) {
|
||||
@ -135,6 +134,7 @@ function (queryDef, _) {
|
||||
}
|
||||
|
||||
var i, filter, condition, queryCondition;
|
||||
|
||||
for (i = 0; i < adhocFilters.length; i++) {
|
||||
filter = adhocFilters[i];
|
||||
condition = {};
|
||||
@ -144,10 +144,12 @@ function (queryDef, _) {
|
||||
|
||||
switch(filter.operator){
|
||||
case "=":
|
||||
_.set(query, "query.bool.must.match_phrase", queryCondition);
|
||||
if (!query.query.bool.must) { query.query.bool.must = []; }
|
||||
query.query.bool.must.push({match_phrase: queryCondition});
|
||||
break;
|
||||
case "!=":
|
||||
_.set(query, "query.bool.must_not.match_phrase", queryCondition);
|
||||
if (!query.query.bool.must_not) { query.query.bool.must_not = []; }
|
||||
query.query.bool.must_not.push({match_phrase: queryCondition});
|
||||
break;
|
||||
case "<":
|
||||
condition[filter.key] = {"lt": filter.value};
|
||||
|
@ -29,7 +29,7 @@ function (_) {
|
||||
|
||||
orderByOptions: [
|
||||
{text: "Doc Count", value: '_count' },
|
||||
{text: "Term value<script>alert('hello')</script>", value: '_term' },
|
||||
{text: "Term value", value: '_term' },
|
||||
],
|
||||
|
||||
orderOptions: [
|
||||
|
10
public/app/plugins/datasource/elasticsearch/query_help.md
Normal file
10
public/app/plugins/datasource/elasticsearch/query_help.md
Normal file
@ -0,0 +1,10 @@
|
||||
#### Alias patterns
|
||||
- {{term fieldname}} = replaced with value of term group by
|
||||
- {{metric}} = replaced with metric name (ex. Average, Min, Max)
|
||||
- {{field}} = replaced with the metric field name
|
||||
|
||||
#### Documentation links
|
||||
|
||||
[Grafana's Elasticsearch Documentation](http://docs.grafana.org/features/datasources/elasticsearch)
|
||||
|
||||
[Official Elasticsearch Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html)
|
@ -40,8 +40,7 @@ describe('ElasticQueryBuilder', function() {
|
||||
var query = builder.build({
|
||||
metrics: [{type: 'count', id: '1'}],
|
||||
timeField: '@timestamp',
|
||||
bucketAggs: [
|
||||
{type: 'terms', field: '@host', id: '2'},
|
||||
bucketAggs: [ {type: 'terms', field: '@host', id: '2'},
|
||||
{type: 'date_histogram', field: '@timestamp', id: '3'}
|
||||
],
|
||||
});
|
||||
@ -282,6 +281,7 @@ describe('ElasticQueryBuilder', function() {
|
||||
bucketAggs: [{type: 'date_histogram', field: '@timestamp', id: '3'}],
|
||||
}, [
|
||||
{key: 'key1', operator: '=', value: 'value1'},
|
||||
{key: 'key2', operator: '=', value: 'value2'},
|
||||
{key: 'key2', operator: '!=', value: 'value2'},
|
||||
{key: 'key3', operator: '<', value: 'value3'},
|
||||
{key: 'key4', operator: '>', value: 'value4'},
|
||||
@ -289,8 +289,9 @@ describe('ElasticQueryBuilder', function() {
|
||||
{key: 'key6', operator: '!~', value: 'value6'},
|
||||
]);
|
||||
|
||||
expect(query.query.bool.must.match_phrase["key1"].query).to.be("value1");
|
||||
expect(query.query.bool.must_not.match_phrase["key2"].query).to.be("value2");
|
||||
expect(query.query.bool.must[0].match_phrase["key1"].query).to.be("value1");
|
||||
expect(query.query.bool.must[1].match_phrase["key2"].query).to.be("value2");
|
||||
expect(query.query.bool.must_not[0].match_phrase["key2"].query).to.be("value2");
|
||||
expect(query.query.bool.filter[2].range["key3"].lt).to.be("value3");
|
||||
expect(query.query.bool.filter[3].range["key4"].gt).to.be("value4");
|
||||
expect(query.query.bool.filter[4].regexp["key5"]).to.be("value5");
|
||||
|
@ -9,6 +9,7 @@ export class GraphiteConfigCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
this.current.jsonData = this.current.jsonData || {};
|
||||
this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,19 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.render_method = instanceSettings.render_method || 'POST';
|
||||
|
||||
this.getQueryOptionsInfo = function() {
|
||||
return {
|
||||
"maxDataPoints": true,
|
||||
"cacheTimeout": true,
|
||||
"links": [
|
||||
{
|
||||
text: "Help",
|
||||
url: "http://docs.grafana.org/features/datasources/graphite/#using-graphite-in-grafana"
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
this.query = function(options) {
|
||||
var graphOptions = {
|
||||
from: this.translateTime(options.rangeRaw.from, false),
|
||||
|
@ -139,8 +139,8 @@ function (_, $) {
|
||||
addFuncDef({
|
||||
name: 'percentileOfSeries',
|
||||
category: categories.Combine,
|
||||
params: [{ name: "n", type: "int" }, { name: "interpolate", type: "select", options: ["true", "false"] }],
|
||||
defaultParams: [95, "false"]
|
||||
params: [{ name: 'n', type: 'int' }, { name: 'interpolate', type: 'boolean', options: ['true', 'false'] }],
|
||||
defaultParams: [95, 'false']
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
@ -261,8 +261,8 @@ function (_, $) {
|
||||
addFuncDef({
|
||||
name: 'sortByName',
|
||||
category: categories.Special,
|
||||
params: [{ name: "natural", type: "select", options: ["true", "false"], optional: true }],
|
||||
defaultParams: ["false"]
|
||||
params: [{ name: 'natural', type: 'boolean', options: ['true', 'false'], optional: true }],
|
||||
defaultParams: ['false']
|
||||
});
|
||||
|
||||
addFuncDef({
|
||||
|
@ -2,10 +2,6 @@ import {GraphiteDatasource} from './datasource';
|
||||
import {GraphiteQueryCtrl} from './query_ctrl';
|
||||
import {GraphiteConfigCtrl} from './config_ctrl';
|
||||
|
||||
class GraphiteQueryOptionsCtrl {
|
||||
static templateUrl = 'partials/query.options.html';
|
||||
}
|
||||
|
||||
class AnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
}
|
||||
@ -14,7 +10,6 @@ export {
|
||||
GraphiteDatasource as Datasource,
|
||||
GraphiteQueryCtrl as QueryCtrl,
|
||||
GraphiteConfigCtrl as ConfigCtrl,
|
||||
GraphiteQueryOptionsCtrl as QueryOptionsCtrl,
|
||||
AnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||
};
|
||||
|
||||
|
@ -1,123 +0,0 @@
|
||||
<section class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-15">
|
||||
<span class="gf-form-label width-8">
|
||||
Cache timeout
|
||||
</span>
|
||||
<input type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="ctrl.panelCtrl.panel.cacheTimeout"
|
||||
bs-tooltip="'Graphite parameter to override memcache default timeout (unit is seconds)'"
|
||||
data-placement="right"
|
||||
spellcheck='false'
|
||||
placeholder="60">
|
||||
</input>
|
||||
</div>
|
||||
<div class="gf-form max-width-15">
|
||||
<span class="gf-form-label">Max data points</span>
|
||||
<input type="text"
|
||||
class="gf-form-input"
|
||||
ng-model="ctrl.panelCtrl.panel.maxDataPoints"
|
||||
bs-tooltip="'Override max data points, automatically set to graph width in pixels.'"
|
||||
data-placement="right"
|
||||
ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
|
||||
spellcheck='false'
|
||||
placeholder="auto">
|
||||
</input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-12">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
Shorter legend names
|
||||
</a>
|
||||
</span>
|
||||
<span class="gf-form-label width-12">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
Series as parameters
|
||||
</a>
|
||||
</span>
|
||||
<span class="gf-form-label width-7">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
Stacking
|
||||
</a>
|
||||
</span>
|
||||
<span class="gf-form-label width-8">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
Templating
|
||||
</a>
|
||||
</span>
|
||||
<span class="gf-form-label width-10">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
max data points
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
|
||||
<h5>Shorter legend names</h5>
|
||||
<ul>
|
||||
<li>alias() function to specify a custom series name</li>
|
||||
<li>aliasByNode(2) to alias by a specific part of your metric path</li>
|
||||
<li>aliasByNode(2, -1) you can add multiple segment paths, and use negative index</li>
|
||||
<li>groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 2">
|
||||
<h5>Series as parameter</h5>
|
||||
<ul>
|
||||
<li>Some graphite functions allow you to have many series arguments</li>
|
||||
<li>Use #[A-Z] to use a graphite query as parameter to a function</li>
|
||||
<li>
|
||||
Examples:
|
||||
<ul>
|
||||
<li>asPercent(#A, #B)</li>
|
||||
<li>prod.srv-01.counters.count - asPercent(#A) : percentage of count in comparison with A query</li>
|
||||
<li>prod.srv-01.counters.count - sumSeries(#A) : sum count and series A </li>
|
||||
<li>divideSeries(#A, #B)</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>If a query is added only to be used as a parameter, hide it from the graph with the eye icon</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 3">
|
||||
<h5>Stacking</h5>
|
||||
<ul>
|
||||
<li>You find the stacking option under Display Styles tab</li>
|
||||
<li>When stacking is enabled make sure null point mode is set to 'null as zero'</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 4">
|
||||
<h5>Templating</h5>
|
||||
<ul>
|
||||
<li>You can use a template variable in place of metric names</li>
|
||||
<li>You can use a template variable in place of function parameters</li>
|
||||
<li>You enable the templating feature in Dashboard settings / Feature toggles </li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 5">
|
||||
<h5>Max data points</h5>
|
||||
<ul>
|
||||
<li>Every graphite request is issued with a maxDataPoints parameter</li>
|
||||
<li>Graphite uses this parameter to consolidate the real number of values down to this number</li>
|
||||
<li>If there are more real values, then by default they will be consolidated using averages</li>
|
||||
<li>This could hide real peaks and max values in your series</li>
|
||||
<li>You can change how point consolidation is made using the consolidateBy graphite function</li>
|
||||
<li>Point consolidation will effect series legend values (min,max,total,current)</li>
|
||||
<li>If you override maxDataPoint and set a high value performance can be severely effected</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
@ -11,6 +11,11 @@
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
|
||||
"queryOptions": {
|
||||
"maxDataPoints": true,
|
||||
"cacheTimeout": true
|
||||
},
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
|
30
public/app/plugins/datasource/graphite/query_help.md
Normal file
30
public/app/plugins/datasource/graphite/query_help.md
Normal file
@ -0,0 +1,30 @@
|
||||
#### Get Shorter legend names
|
||||
|
||||
- alias() function to specify a custom series name
|
||||
- aliasByNode(2) to alias by a specific part of your metric path
|
||||
- groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by.
|
||||
|
||||
#### Series as parameter
|
||||
|
||||
- Some graphite functions allow you to have many series arguments
|
||||
- Use #[A-Z] to use a graphite query as parameter to a function
|
||||
- Examples:
|
||||
- asPercent(#A, #B)
|
||||
- divideSeries(#A, #B)
|
||||
|
||||
If a query is added only to be used as a parameter, hide it from the graph with the eye icon
|
||||
|
||||
#### Max data points
|
||||
- Every graphite request is issued with a maxDataPoints parameter
|
||||
- Graphite uses this parameter to consolidate the real number of values down to this number
|
||||
- If there are more real values, then by default they will be consolidated using averages
|
||||
- This could hide real peaks and max values in your series
|
||||
- You can change how point consolidation is made using the consolidateBy graphite function
|
||||
- Point consolidation will effect series legend values (min,max,total,current)
|
||||
- if you override maxDataPoint and set a high value performance can be severely effected
|
||||
|
||||
#### Documentation links:
|
||||
|
||||
[Grafana's Graphite Documentation](http://docs.grafana.org/features/datasources/graphite)
|
||||
|
||||
[Official Graphite Documentation](https://graphite.readthedocs.io)
|
@ -229,8 +229,11 @@ export default class InfluxQuery {
|
||||
return this.renderTagCondition(tag, index, interpolate);
|
||||
});
|
||||
|
||||
query += conditions.join(' ');
|
||||
query += (conditions.length > 0 ? ' AND ' : '') + '$timeFilter';
|
||||
if (conditions.length > 0) {
|
||||
query += '(' + conditions.join(' ') + ') AND ';
|
||||
}
|
||||
|
||||
query += '$timeFilter';
|
||||
|
||||
var groupBySection = "";
|
||||
for (i = 0; i < this.groupByParts.length; i++) {
|
||||
|
@ -5,10 +5,6 @@ class InfluxConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
}
|
||||
|
||||
class InfluxQueryOptionsCtrl {
|
||||
static templateUrl = 'partials/query.options.html';
|
||||
}
|
||||
|
||||
class InfluxAnnotationsQueryCtrl {
|
||||
static templateUrl = 'partials/annotations.editor.html';
|
||||
}
|
||||
@ -17,7 +13,6 @@ export {
|
||||
InfluxDatasource as Datasource,
|
||||
InfluxQueryCtrl as QueryCtrl,
|
||||
InfluxConfigCtrl as ConfigCtrl,
|
||||
InfluxQueryOptionsCtrl as QueryOptionsCtrl,
|
||||
InfluxAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||
};
|
||||
|
||||
|
@ -24,10 +24,14 @@
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label">Default group by time</span>
|
||||
<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval"
|
||||
spellcheck='false' placeholder="example: >10s"></input>
|
||||
<i class="fa fa-question-circle" bs-tooltip="'Set a low limit by having a greater sign: example: >10s'" data-placement="right"></i>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Min time interval</span>
|
||||
<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="10s"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
A lower limit for the auto group by time interval. Recommended to be set to write frequency,
|
||||
for example <code>1m</code> if your data is written every minute.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,76 +0,0 @@
|
||||
<section class="grafana-metric-options">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label"><i class="fa fa-wrench"></i></span>
|
||||
<span class="gf-form-label width-11">Group by time interval</span>
|
||||
<input type="text" class="gf-form-input width-16" ng-model="ctrl.panelCtrl.panel.interval" ng-blur="ctrl.panelCtrl.refresh();"
|
||||
spellcheck='false' placeholder="example: >10s">
|
||||
<info-popover mode="right-absolute">
|
||||
Set a low limit by having a greater sign: example: >60s
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">
|
||||
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
alias patterns
|
||||
</a>
|
||||
</span>
|
||||
<span class="gf-form-label width-10">
|
||||
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
stacking & fill
|
||||
</a>
|
||||
</span>
|
||||
<span class="gf-form-label width-10">
|
||||
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
group by time
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
|
||||
<h5>Alias patterns</h5>
|
||||
<ul>
|
||||
<li>$m = replaced with measurement name</li>
|
||||
<li>$measurement = replaced with measurement name</li>
|
||||
<li>$1 - $9 = replaced with part of measurement name (if you separate your measurement name with dots)</li>
|
||||
<li>$col = replaced with column name</li>
|
||||
<li>$tag_exampletag = replaced with the value of the <i>exampletag</i> tag</li>
|
||||
<li>You can also use [[tag_exampletag]] pattern replacement syntax</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 2">
|
||||
<h5>Stacking and fill</h5>
|
||||
<ul>
|
||||
<li>When stacking is enabled it is important that points align</li>
|
||||
<li>If there are missing points for one series it can cause gaps or missing bars</li>
|
||||
<li>You must use fill(0), and select a group by time low limit</li>
|
||||
<li>Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds</li>
|
||||
<li>This will insert zeros for series that are missing measurements and will make stacking work properly</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="grafana-info-box span6" ng-if="ctrl.panelCtrl.editorHelpIndex === 3">
|
||||
<h5>Group by time</h5>
|
||||
<ul>
|
||||
<li>Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana</li>
|
||||
<li>Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph</li>
|
||||
<li>If you use fill(0) or fill(null) set a low limit for the auto group by time interval</li>
|
||||
<li>The low limit can only be set in the group by time option below your queries</li>
|
||||
<li>You set a low limit by adding a greater sign before the interval</li>
|
||||
<li>Example: >60s if you write metrics to InfluxDB every 60 seconds</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -8,6 +8,10 @@
|
||||
"annotations": true,
|
||||
"alerting": true,
|
||||
|
||||
"queryOptions": {
|
||||
"minInterval": true
|
||||
},
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
"name": "Grafana Project",
|
||||
|
28
public/app/plugins/datasource/influxdb/query_help.md
Normal file
28
public/app/plugins/datasource/influxdb/query_help.md
Normal file
@ -0,0 +1,28 @@
|
||||
#### Alias patterns
|
||||
- replaced with measurement name
|
||||
- $measurement = replaced with measurement name
|
||||
- $1 - $9 = replaced with part of measurement name (if you separate your measurement name with dots)
|
||||
- $col = replaced with column name
|
||||
- $tag_exampletag = replaced with the value of the <i>exampletag</i> tag
|
||||
- You can also use [[tag_exampletag]] pattern replacement syntax
|
||||
|
||||
#### Stacking and fill
|
||||
- When stacking is enabled it is important that points align
|
||||
- If there are missing points for one series it can cause gaps or missing bars
|
||||
- You must use fill(0), and select a group by time low limit
|
||||
- Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds
|
||||
- This will insert zeros for series that are missing measurements and will make stacking work properly
|
||||
|
||||
#### Group by time
|
||||
- Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana
|
||||
- Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
|
||||
- If you use fill(0) or fill(null) set a low limit for the auto group by time interval
|
||||
- The low limit can only be set in the group by time option below your queries
|
||||
- You set a low limit by adding a greater sign before the interval
|
||||
- Example: >60s if you write metrics to InfluxDB every 60 seconds
|
||||
|
||||
#### Documentation links:
|
||||
|
||||
[Grafana's InfluxDB Documentation](http://docs.grafana.org/features/datasources/influxdb)
|
||||
|
||||
[Official InfluxDB Documentation](https://docs.influxdata.com/influxdb)
|
@ -57,7 +57,7 @@ describe('InfluxQuery', function() {
|
||||
|
||||
var queryText = query.render();
|
||||
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server\\\\1\' AND $timeFilter'
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE ("hostname" = \'server\\\\1\') AND $timeFilter'
|
||||
+ ' GROUP BY time($__interval)');
|
||||
});
|
||||
|
||||
@ -69,7 +69,7 @@ describe('InfluxQuery', function() {
|
||||
}, templateSrv, {});
|
||||
|
||||
var queryText = query.render();
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "app" =~ /e.*/ AND $timeFilter GROUP BY time($__interval)');
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE ("app" =~ /e.*/) AND $timeFilter GROUP BY time($__interval)');
|
||||
});
|
||||
});
|
||||
|
||||
@ -82,7 +82,7 @@ describe('InfluxQuery', function() {
|
||||
}, templateSrv, {});
|
||||
|
||||
var queryText = query.render();
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' AND "app" = \'email\' AND ' +
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE ("hostname" = \'server1\' AND "app" = \'email\') AND ' +
|
||||
'$timeFilter GROUP BY time($__interval)');
|
||||
});
|
||||
});
|
||||
@ -96,7 +96,7 @@ describe('InfluxQuery', function() {
|
||||
}, templateSrv, {});
|
||||
|
||||
var queryText = query.render();
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "hostname" = \'server1\' OR "hostname" = \'server2\' AND ' +
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE ("hostname" = \'server1\' OR "hostname" = \'server2\') AND ' +
|
||||
'$timeFilter GROUP BY time($__interval)');
|
||||
});
|
||||
});
|
||||
@ -110,7 +110,7 @@ describe('InfluxQuery', function() {
|
||||
}, templateSrv, {});
|
||||
|
||||
var queryText = query.render();
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE "value" > 5 AND $timeFilter');
|
||||
expect(queryText).to.be('SELECT mean("value") FROM "cpu" WHERE ("value" > 5) AND $timeFilter');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
<query-editor-row query-ctrl="ctrl" can-collapse="false">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<textarea rows="10" class="gf-form-input" ng-model="ctrl.target.rawSql" spellcheck="false" placeholder="query expression" data-min-length=0 data-items=100 ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"></textarea>
|
||||
<code-editor content="ctrl.target.rawSql" on-change="ctrl.panelCtrl.refresh()" data-mode="sql">
|
||||
</code-editor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
42
public/app/plugins/datasource/prometheus/completer.ts
Normal file
42
public/app/plugins/datasource/prometheus/completer.ts
Normal file
@ -0,0 +1,42 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import {PrometheusDatasource} from "./datasource";
|
||||
|
||||
export class PromCompleter {
|
||||
identifierRegexps = [/[\[\]a-zA-Z_0-9=]/];
|
||||
|
||||
constructor(private datasource: PrometheusDatasource) {
|
||||
}
|
||||
|
||||
getCompletions(editor, session, pos, prefix, callback) {
|
||||
if (prefix === '[') {
|
||||
var vectors = [];
|
||||
for (let unit of ['s', 'm', 'h']) {
|
||||
for (let value of [1,5,10,30]) {
|
||||
vectors.push({caption: value+unit, value: '['+value+unit, meta: 'range vector'});
|
||||
}
|
||||
}
|
||||
callback(null, vectors);
|
||||
return;
|
||||
}
|
||||
|
||||
var query = prefix;
|
||||
var line = editor.session.getLine(pos.row);
|
||||
|
||||
return this.datasource.performSuggestQuery(query).then(metricNames => {
|
||||
callback(null, metricNames.map(name => {
|
||||
let value = name;
|
||||
if (prefix === '(') {
|
||||
value = '(' + name;
|
||||
}
|
||||
|
||||
return {
|
||||
caption: name,
|
||||
value: value,
|
||||
meta: 'metric',
|
||||
};
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -11,18 +11,37 @@ import TableModel from 'app/core/table_model';
|
||||
|
||||
var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/;
|
||||
|
||||
/** @ngInject */
|
||||
export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) {
|
||||
this.type = 'prometheus';
|
||||
this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
|
||||
this.name = instanceSettings.name;
|
||||
this.supportMetrics = true;
|
||||
this.url = instanceSettings.url;
|
||||
this.directUrl = instanceSettings.directUrl;
|
||||
this.basicAuth = instanceSettings.basicAuth;
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
function prometheusSpecialRegexEscape(value) {
|
||||
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
|
||||
}
|
||||
|
||||
this._request = function(method, url, requestId) {
|
||||
export class PrometheusDatasource {
|
||||
type: string;
|
||||
editorSrc: string;
|
||||
name: string;
|
||||
supportMetrics: boolean;
|
||||
url: string;
|
||||
directUrl: string;
|
||||
basicAuth: any;
|
||||
withCredentials: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(instanceSettings,
|
||||
private $q,
|
||||
private backendSrv,
|
||||
private templateSrv,
|
||||
private timeSrv) {
|
||||
this.type = 'prometheus';
|
||||
this.editorSrc = 'app/features/prometheus/partials/query.editor.html';
|
||||
this.name = instanceSettings.name;
|
||||
this.supportMetrics = true;
|
||||
this.url = instanceSettings.url;
|
||||
this.directUrl = instanceSettings.directUrl;
|
||||
this.basicAuth = instanceSettings.basicAuth;
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
}
|
||||
|
||||
_request(method, url, requestId?) {
|
||||
var options: any = {
|
||||
url: this.url + url,
|
||||
method: method,
|
||||
@ -32,20 +51,17 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
if (this.basicAuth || this.withCredentials) {
|
||||
options.withCredentials = true;
|
||||
}
|
||||
|
||||
if (this.basicAuth) {
|
||||
options.headers = {
|
||||
"Authorization": this.basicAuth
|
||||
};
|
||||
}
|
||||
|
||||
return backendSrv.datasourceRequest(options);
|
||||
};
|
||||
|
||||
function prometheusSpecialRegexEscape(value) {
|
||||
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
|
||||
return this.backendSrv.datasourceRequest(options);
|
||||
}
|
||||
|
||||
this.interpolateQueryExpr = function(value, variable, defaultFormatFn) {
|
||||
interpolateQueryExpr(value, variable, defaultFormatFn) {
|
||||
// if no multi or include all do not regexEscape
|
||||
if (!variable.multi && !variable.includeAll) {
|
||||
return value;
|
||||
@ -57,14 +73,13 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
|
||||
var escapedValues = _.map(value, prometheusSpecialRegexEscape);
|
||||
return escapedValues.join('|');
|
||||
};
|
||||
}
|
||||
|
||||
this.targetContainsTemplate = function(target) {
|
||||
return templateSrv.variableExists(target.expr);
|
||||
};
|
||||
targetContainsTemplate(target) {
|
||||
return this.templateSrv.variableExists(target.expr);
|
||||
}
|
||||
|
||||
// Called once per panel (graph)
|
||||
this.query = function(options) {
|
||||
query(options) {
|
||||
var self = this;
|
||||
var start = this.getPrometheusTime(options.range.from, false);
|
||||
var end = this.getPrometheusTime(options.range.to, true);
|
||||
@ -82,10 +97,10 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
activeTargets.push(target);
|
||||
|
||||
var query: any = {};
|
||||
query.expr = templateSrv.replace(target.expr, options.scopedVars, self.interpolateQueryExpr);
|
||||
query.expr = this.templateSrv.replace(target.expr, options.scopedVars, self.interpolateQueryExpr);
|
||||
query.requestId = options.panelId + target.refId;
|
||||
|
||||
var interval = templateSrv.replace(target.interval, options.scopedVars) || options.interval;
|
||||
var interval = this.templateSrv.replace(target.interval, options.scopedVars) || options.interval;
|
||||
var intervalFactor = target.intervalFactor || 1;
|
||||
target.step = query.step = this.calculateInterval(interval, intervalFactor);
|
||||
var range = Math.ceil(end - start);
|
||||
@ -95,14 +110,14 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
|
||||
// No valid targets, return the empty result to save a round trip.
|
||||
if (_.isEmpty(queries)) {
|
||||
return $q.when({ data: [] });
|
||||
return this.$q.when({ data: [] });
|
||||
}
|
||||
|
||||
var allQueryPromise = _.map(queries, query => {
|
||||
return this.performTimeSeriesQuery(query, start, end);
|
||||
});
|
||||
|
||||
return $q.all(allQueryPromise).then(responseList => {
|
||||
return this.$q.all(allQueryPromise).then(responseList => {
|
||||
var result = [];
|
||||
var index = 0;
|
||||
|
||||
@ -122,27 +137,27 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
|
||||
return { data: result };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.adjustStep = function(step, autoStep, range) {
|
||||
adjustStep(step, autoStep, range) {
|
||||
// Prometheus drop query if range/step > 11000
|
||||
// calibrate step if it is too big
|
||||
if (step !== 0 && range / step > 11000) {
|
||||
step = Math.ceil(range / 11000);
|
||||
}
|
||||
return Math.max(step, autoStep);
|
||||
};
|
||||
}
|
||||
|
||||
this.performTimeSeriesQuery = function(query, start, end) {
|
||||
performTimeSeriesQuery(query, start, end) {
|
||||
if (start > end) {
|
||||
throw { message: 'Invalid time range' };
|
||||
}
|
||||
|
||||
var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end + '&step=' + query.step;
|
||||
return this._request('GET', url, query.requestId);
|
||||
};
|
||||
}
|
||||
|
||||
this.performSuggestQuery = function(query) {
|
||||
performSuggestQuery(query) {
|
||||
var url = '/api/v1/label/__name__/values';
|
||||
|
||||
return this._request('GET', url).then(function(result) {
|
||||
@ -150,41 +165,30 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
return metricName.indexOf(query) !== 1;
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.metricFindQuery = function(query) {
|
||||
if (!query) { return $q.when([]); }
|
||||
metricFindQuery(query) {
|
||||
if (!query) { return this.$q.when([]); }
|
||||
|
||||
var interpolated;
|
||||
try {
|
||||
interpolated = templateSrv.replace(query, {}, this.interpolateQueryExpr);
|
||||
} catch (err) {
|
||||
return $q.reject(err);
|
||||
}
|
||||
|
||||
var metricFindQuery = new PrometheusMetricFindQuery(this, interpolated, timeSrv);
|
||||
let interpolated = this.templateSrv.replace(query, {}, this.interpolateQueryExpr);
|
||||
var metricFindQuery = new PrometheusMetricFindQuery(this, interpolated, this.timeSrv);
|
||||
return metricFindQuery.process();
|
||||
};
|
||||
}
|
||||
|
||||
this.annotationQuery = function(options) {
|
||||
annotationQuery(options) {
|
||||
var annotation = options.annotation;
|
||||
var expr = annotation.expr || '';
|
||||
var tagKeys = annotation.tagKeys || '';
|
||||
var titleFormat = annotation.titleFormat || '';
|
||||
var textFormat = annotation.textFormat || '';
|
||||
|
||||
if (!expr) { return $q.when([]); }
|
||||
if (!expr) { return this.$q.when([]); }
|
||||
|
||||
var interpolated;
|
||||
try {
|
||||
interpolated = templateSrv.replace(expr, {}, this.interpolateQueryExpr);
|
||||
} catch (err) {
|
||||
return $q.reject(err);
|
||||
}
|
||||
var interpolated = this.templateSrv.replace(expr, {}, this.interpolateQueryExpr);
|
||||
|
||||
var step = '60s';
|
||||
if (annotation.step) {
|
||||
step = templateSrv.replace(annotation.step);
|
||||
step = this.templateSrv.replace(annotation.step);
|
||||
}
|
||||
|
||||
var start = this.getPrometheusTime(options.range.from, false);
|
||||
@ -222,19 +226,19 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
|
||||
return eventList;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.testDatasource = function() {
|
||||
testDatasource() {
|
||||
return this.metricFindQuery('metrics(.*)').then(function() {
|
||||
return { status: 'success', message: 'Data source is working', title: 'Success' };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.calculateInterval = function(interval, intervalFactor) {
|
||||
calculateInterval(interval, intervalFactor) {
|
||||
return Math.ceil(this.intervalSeconds(interval) * intervalFactor);
|
||||
};
|
||||
}
|
||||
|
||||
this.intervalSeconds = function(interval) {
|
||||
intervalSeconds(interval) {
|
||||
var m = interval.match(durationSplitRegexp);
|
||||
var dur = moment.duration(parseInt(m[1]), m[2]);
|
||||
var sec = dur.asSeconds();
|
||||
@ -243,9 +247,9 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
}
|
||||
|
||||
return sec;
|
||||
};
|
||||
}
|
||||
|
||||
this.transformMetricData = function(md, options, start, end) {
|
||||
transformMetricData(md, options, start, end) {
|
||||
var dps = [],
|
||||
metricLabel = null;
|
||||
|
||||
@ -273,9 +277,9 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
}
|
||||
|
||||
return { target: metricLabel, datapoints: dps };
|
||||
};
|
||||
}
|
||||
|
||||
this.transformMetricDataToTable = function(md) {
|
||||
transformMetricDataToTable(md) {
|
||||
var table = new TableModel();
|
||||
var i, j;
|
||||
var metricLabels = {};
|
||||
@ -325,17 +329,17 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
});
|
||||
|
||||
return table;
|
||||
};
|
||||
}
|
||||
|
||||
this.createMetricLabel = function(labelData, options) {
|
||||
createMetricLabel(labelData, options) {
|
||||
if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) {
|
||||
return this.getOriginalMetricName(labelData);
|
||||
}
|
||||
|
||||
return this.renderTemplate(templateSrv.replace(options.legendFormat), labelData) || '{}';
|
||||
};
|
||||
return this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData) || '{}';
|
||||
}
|
||||
|
||||
this.renderTemplate = function(aliasPattern, aliasData) {
|
||||
renderTemplate(aliasPattern, aliasData) {
|
||||
var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g;
|
||||
return aliasPattern.replace(aliasRegex, function(match, g1) {
|
||||
if (aliasData[g1]) {
|
||||
@ -343,21 +347,21 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
}
|
||||
return g1;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
this.getOriginalMetricName = function(labelData) {
|
||||
getOriginalMetricName(labelData) {
|
||||
var metricName = labelData.__name__ || '';
|
||||
delete labelData.__name__;
|
||||
var labelPart = _.map(_.toPairs(labelData), function(label) {
|
||||
return label[0] + '="' + label[1] + '"';
|
||||
}).join(',');
|
||||
return metricName + '{' + labelPart + '}';
|
||||
};
|
||||
}
|
||||
|
||||
this.getPrometheusTime = function(date, roundUp) {
|
||||
getPrometheusTime(date, roundUp) {
|
||||
if (_.isString(date)) {
|
||||
date = dateMath.parse(date, roundUp);
|
||||
}
|
||||
return Math.ceil(date.valueOf() / 1000);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
<query-editor-row query-ctrl="ctrl" can-collapse="true" has-text-edit-mode="false">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<textarea rows="3" class="gf-form-input" ng-model="ctrl.target.expr" spellcheck="false" placeholder="query expression" data-min-length=0 data-items=100 give-focus="ctrl.target.refId == 'A'" ng-model-onblur ng-change="ctrl.refreshMetricData()"></textarea>
|
||||
<code-editor content="ctrl.target.expr" on-change="ctrl.refreshMetricData()"
|
||||
get-completer="ctrl.getCompleter()" data-mode="prometheus">
|
||||
</code-editor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -38,17 +40,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<div class="gf-form-label gf-form-label--grow"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-26">
|
||||
<label class="gf-form-label width-8">Metric lookup</label>
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.target.metric" spellcheck='false' bs-typeahead="ctrl.suggestMetrics" placeholder="metric name" data-min-length=0 data-items=100>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-6">Format as</label>
|
||||
<div class="gf-form-select-wrapper width-8">
|
||||
@ -66,4 +57,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</query-editor-row>
|
||||
|
@ -6,6 +6,7 @@ import moment from 'moment';
|
||||
|
||||
import * as dateMath from 'app/core/utils/datemath';
|
||||
import {QueryCtrl} from 'app/plugins/sdk';
|
||||
import {PromCompleter} from './completer';
|
||||
|
||||
class PrometheusQueryCtrl extends QueryCtrl {
|
||||
static templateUrl = 'partials/query.editor.html';
|
||||
@ -15,6 +16,7 @@ class PrometheusQueryCtrl extends QueryCtrl {
|
||||
formats: any;
|
||||
oldTarget: any;
|
||||
suggestMetrics: any;
|
||||
getMetricsAutocomplete: any;
|
||||
linkToPrometheus: any;
|
||||
|
||||
/** @ngInject */
|
||||
@ -36,24 +38,19 @@ class PrometheusQueryCtrl extends QueryCtrl {
|
||||
{text: 'Table', value: 'table'},
|
||||
];
|
||||
|
||||
$scope.$on('typeahead-updated', () => {
|
||||
this.$scope.$apply(() => {
|
||||
this.target.expr += this.target.metric;
|
||||
this.metric = '';
|
||||
this.refreshMetricData();
|
||||
});
|
||||
});
|
||||
|
||||
// called from typeahead so need this
|
||||
// here in order to ensure this ref
|
||||
this.suggestMetrics = (query, callback) => {
|
||||
console.log(this);
|
||||
this.datasource.performSuggestQuery(query).then(callback);
|
||||
};
|
||||
|
||||
this.updateLink();
|
||||
}
|
||||
|
||||
getCompleter(query) {
|
||||
return new PromCompleter(this.datasource);
|
||||
// console.log('getquery);
|
||||
// return this.datasource.performSuggestQuery(query).then(res => {
|
||||
// return res.map(item => {
|
||||
// return {word: item, type: 'metric'};
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
getDefaultFormat() {
|
||||
if (this.panelCtrl.panel.type === 'table') {
|
||||
return 'table';
|
||||
|
@ -33,6 +33,7 @@ System.config({
|
||||
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
|
||||
"d3": "vendor/d3/d3.js",
|
||||
"jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
|
||||
"ace": "vendor/npm/ace-builds/src-noconflict/ace"
|
||||
},
|
||||
|
||||
packages: {
|
||||
@ -73,5 +74,9 @@ System.config({
|
||||
format: 'global',
|
||||
exports: 'Mousetrap'
|
||||
},
|
||||
'vendor/npm/ace-builds/src-noconflict/ace.js': {
|
||||
format: 'global',
|
||||
exports: 'ace'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -77,6 +77,7 @@
|
||||
@import "components/row.scss";
|
||||
@import "components/json_explorer.scss";
|
||||
@import "components/collapse_box.scss";
|
||||
@import "components/code_editor.scss";
|
||||
|
||||
// PAGES
|
||||
@import "pages/login";
|
||||
|
@ -276,7 +276,7 @@ $card-background-hover: linear-gradient(135deg, #343434, #262626);
|
||||
$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3);
|
||||
|
||||
// info box
|
||||
$info-box-background: linear-gradient(120deg, #142749, #0e203e);
|
||||
$info-box-background: linear-gradient(100deg, #1a4552, #0b2127);
|
||||
|
||||
// footer
|
||||
$footer-link-color: $gray-1;
|
||||
|
79
public/sass/components/_code_editor.scss
Normal file
79
public/sass/components/_code_editor.scss
Normal file
@ -0,0 +1,79 @@
|
||||
.gf-code-editor {
|
||||
min-height: 2.60rem;
|
||||
min-width: 20rem;
|
||||
flex-grow: 1;
|
||||
margin-right: 0.25rem;
|
||||
visibility: hidden;
|
||||
|
||||
&.ace_editor {
|
||||
@include font-family-monospace();
|
||||
font-size: 1rem;
|
||||
min-height: 2.60rem;
|
||||
|
||||
@include border-radius($input-border-radius-sm);
|
||||
border: $input-btn-border-width solid $input-border-color;
|
||||
}
|
||||
|
||||
&--theme-loaded {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.ace_editor.ace_autocomplete {
|
||||
@include font-family-monospace();
|
||||
font-size: 1rem;
|
||||
|
||||
// Ace editor adds <style> tag at the end of <head>, after grafana.css, so !important
|
||||
// is used for overriding styles with the same CSS specificity.
|
||||
background-color: $dropdownBackground !important;
|
||||
color: $dropdownLinkColor !important;
|
||||
border: 1px solid $dropdownBorder !important;
|
||||
width: 320px !important;
|
||||
|
||||
.ace_scroller {
|
||||
.ace_selected, .ace_active-line, .ace_line-hover {
|
||||
color: $dropdownLinkColorHover;
|
||||
background-color: $dropdownLinkBackgroundHover !important;
|
||||
}
|
||||
|
||||
.ace_line-hover {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.ace_completion-highlight {
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
.ace_rightAlignedText {
|
||||
color: $text-muted;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$doc-font-size: $font-size-sm;
|
||||
|
||||
.ace_tooltip.ace_doc-tooltip {
|
||||
@include font-family-monospace();
|
||||
font-size: $doc-font-size;
|
||||
|
||||
background-color: $popover-help-bg;
|
||||
color: $popover-help-color;
|
||||
background-image: none;
|
||||
border: 1px solid $dropdownBorder;
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
hr {
|
||||
background-color: $popover-help-color;
|
||||
margin: 0.5rem 0rem;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0px 1px;
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ace_tooltip {
|
||||
border-radius: 3px;
|
||||
}
|
@ -16,6 +16,10 @@ $gf-form-margin: 0.25rem;
|
||||
&--grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&--flex-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.gf-form-disabled {
|
||||
@ -54,7 +58,6 @@ $gf-form-margin: 0.25rem;
|
||||
background-color: $input-label-bg;
|
||||
display: block;
|
||||
font-size: $font-size-sm;
|
||||
margin-right: $gf-form-margin;
|
||||
|
||||
border: $input-btn-border-width solid transparent;
|
||||
@include border-radius($label-border-radius-sm);
|
||||
@ -103,7 +106,6 @@ $gf-form-margin: 0.25rem;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
margin-right: $gf-form-margin;
|
||||
font-size: $font-size-base;
|
||||
margin-right: $gf-form-margin;
|
||||
line-height: $input-line-height;
|
||||
color: $input-color;
|
||||
background-color: $input-bg;
|
||||
@ -112,7 +114,6 @@ $gf-form-margin: 0.25rem;
|
||||
border: $input-btn-border-width solid $input-border-color;
|
||||
@include border-radius($input-border-radius-sm);
|
||||
@include box-shadow($input-box-shadow);
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -235,7 +236,6 @@ $gf-form-margin: 0.25rem;
|
||||
font-size: $font-size-sm;
|
||||
box-shadow: none;
|
||||
border: $input-btn-border-width solid transparent;
|
||||
@include border-radius($label-border-radius-sm);
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
@ -281,7 +281,7 @@ $gf-form-margin: 0.25rem;
|
||||
&--right-absolute {
|
||||
position: absolute;
|
||||
right: $spacer;
|
||||
top: 8px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
&--right-normal {
|
||||
|
@ -1,12 +1,12 @@
|
||||
.grafana-info-box::before {
|
||||
content: "\f05a";
|
||||
font-family:'FontAwesome';
|
||||
position: absolute;
|
||||
top: -13px;
|
||||
left: -8px;
|
||||
font-size: 20px;
|
||||
color: $text-color;
|
||||
}
|
||||
// .grafana-info-box::before {
|
||||
// content: "\f05a";
|
||||
// font-family:'FontAwesome';
|
||||
// position: absolute;
|
||||
// top: -13px;
|
||||
// left: -8px;
|
||||
// font-size: 20px;
|
||||
// color: $text-color;
|
||||
// }
|
||||
|
||||
.grafana-info-box {
|
||||
position: relative;
|
||||
@ -15,12 +15,14 @@
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: $spacer;
|
||||
margin-right: $gf-form-margin;
|
||||
flex-grow: 1;
|
||||
|
||||
h5 {
|
||||
margin-bottom: $spacer;
|
||||
}
|
||||
ul {
|
||||
padding-left: $spacer;
|
||||
padding-left: $spacer * 1.5;
|
||||
}
|
||||
|
||||
a {
|
||||
@ -28,3 +30,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.grafana-info-box__close {
|
||||
text-align: center;
|
||||
display: block;
|
||||
color: $link-color !important;
|
||||
height: 0;
|
||||
position: relative;
|
||||
top: -9px;
|
||||
}
|
||||
|
@ -181,7 +181,7 @@
|
||||
}
|
||||
|
||||
.graph-legend {
|
||||
margin: 0;
|
||||
margin: 0 0 0 1rem;
|
||||
}
|
||||
|
||||
.graph-legend-series {
|
||||
@ -321,7 +321,7 @@
|
||||
|
||||
.left-yaxis-label {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
left: 2px;
|
||||
transform: translateX(-50%) translateY(-50%) rotate(-90deg);
|
||||
// this is needed for phantomsjs 2.1
|
||||
-webkit-transform: translateX(-50%) translateY(-50%) rotate(-90deg);
|
||||
@ -329,7 +329,7 @@
|
||||
|
||||
.right-yaxis-label {
|
||||
top: 50%;
|
||||
right: 0;
|
||||
right: 2px;
|
||||
transform: translateX(50%) translateY(-50%) rotate(90deg);
|
||||
// this is needed for phantomsjs 2.1
|
||||
-webkit-transform: translateX(50%) translateY(-50%) rotate(90deg);
|
||||
@ -339,7 +339,7 @@
|
||||
display: inline-block;
|
||||
color: $text-color;
|
||||
font-size: $font-size-sm;
|
||||
position: relative;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
@ -67,6 +67,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.gf-query-ds-label {
|
||||
text-align: center;
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.grafana-metric-options {
|
||||
margin-top: 25px;
|
||||
}
|
||||
@ -146,3 +151,25 @@ input[type="text"].tight-form-func-param {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-troubleshooter {
|
||||
font-size: $font-size-sm;
|
||||
margin: $gf-form-margin;
|
||||
border: 1px solid $btn-secondary-bg;
|
||||
min-height: 100px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.query-troubleshooter__header {
|
||||
float: right;
|
||||
font-size: $font-size-sm;
|
||||
text-align: right;
|
||||
padding: $input-padding-y $input-padding-x;
|
||||
a {
|
||||
margin-left: $spacer;
|
||||
}
|
||||
}
|
||||
|
||||
.query-troubleshooter__body {
|
||||
padding: $spacer 0;
|
||||
}
|
||||
|
@ -23,3 +23,14 @@
|
||||
-o-animation: #{$str};
|
||||
animation: #{$str};
|
||||
}
|
||||
|
||||
.animate-height {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&--open {
|
||||
max-height: 1000px;
|
||||
overflow: auto;
|
||||
transition: max-height 250ms ease-in-out;
|
||||
}
|
||||
}
|
||||
|
@ -143,8 +143,8 @@ define([
|
||||
expect(res.intervalMs).to.be(500);
|
||||
});
|
||||
|
||||
it('fixed user interval', function() {
|
||||
var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
|
||||
it('fixed user min interval', function() {
|
||||
var range = {from: dateMath.parse('now-10m'), to: dateMath.parse('now')};
|
||||
var res = kbn.calculateInterval(range, 1600, '10s');
|
||||
expect(res.interval).to.be('10s');
|
||||
expect(res.intervalMs).to.be(10000);
|
||||
|
@ -41,6 +41,7 @@
|
||||
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
|
||||
"d3": "vendor/d3/d3.js",
|
||||
"jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
|
||||
"ace": "vendor/npm/ace-builds/src-noconflict/ace",
|
||||
},
|
||||
|
||||
packages: {
|
||||
@ -73,6 +74,10 @@
|
||||
format: 'global',
|
||||
exports: 'Mousetrap'
|
||||
},
|
||||
'vendor/npm/ace-builds/src-noconflict/ace.js': {
|
||||
format: 'global',
|
||||
exports: 'ace'
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -19,6 +19,7 @@ module.exports = function(config) {
|
||||
cwd: './node_modules',
|
||||
expand: true,
|
||||
src: [
|
||||
'ace-builds/src-noconflict/**/*',
|
||||
'eventemitter3/*.js',
|
||||
'systemjs/dist/*.js',
|
||||
'es6-promise/**/*',
|
||||
|
@ -21,7 +21,10 @@ module.exports = function(config, grunt) {
|
||||
return;
|
||||
}
|
||||
|
||||
gaze(config.srcDir + '/**/*', function(err, watcher) {
|
||||
gaze([
|
||||
config.srcDir + '/app/**/*',
|
||||
config.srcDir + '/sass/**/*',
|
||||
], function(err, watcher) {
|
||||
|
||||
console.log('Gaze watchers setup');
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user