Merged with grafana/master

This commit is contained in:
utkarshcmu 2016-02-05 01:50:47 -08:00
commit 0201b9769c
161 changed files with 6909 additions and 5295 deletions

View File

@ -1,23 +1,27 @@
# 3.0.0 (unrelased master branch)
### New Features
* **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/pull/3655)
* **Playlists**: Playlists can now be persisted and started from urls, closes [#3655](https://github.com/grafana/grafana/issues/3655)
* **Metadata**: Settings panel now shows dashboard metadata, closes [#3304](https://github.com/grafana/grafana/issues/3304)
* **InfluxDB**: Support for policy selection in query editor, closes [#2018](https://github.com/grafana/grafana/issues/2018)
* **Snapshots UI**: Dashboard snapshots list can be managed through UI, closes[#1984](https://github.com/grafana/grafana/issues/1984)
### Breaking changes
* **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring a minor update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
* **Plugin API**: Both datasource and panel plugin api (and plugin.json schema) have been updated, requiring an update to plugins. See [plugin api](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) for more info.
* **InfluxDB 0.8.x** The data source for the old version of influxdb (0.8.x) is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3523](https://github.com/grafana/grafana/issues/3523)
* **KairosDB** The data source is no longer included in default builds, but can easily be installed via improved plugin system, closes [#3524](https://github.com/grafana/grafana/issues/3524)
### Enhancements
* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/pull/3458)
* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/pull/3584)
* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/pull/3635)
* **Sessions**: Support for memcached as session storage, closes [#3458](https://github.com/grafana/grafana/issues/3458)
* **mysql**: Grafana now supports ssl for mysql, closes [#3584](https://github.com/grafana/grafana/issues/3584)
* **snapshot**: Annotations are now included in snapshots, closes [#3635](https://github.com/grafana/grafana/issues/3635)
* **Admin**: Admin can now have global overview of Grafana setup, closes [#3812](https://github.com/grafana/grafana/issues/3812)
### Bug fixes
* **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794)
* **InfluxDB**: Fix for InfluxDB and table panel when using Format As Table and having group by time, fixes [#3928](https://github.com/grafana/grafana/issues/3928)
* **Panel Time shift**: Fix for panel time range and using dashboard times liek `Today` and `This Week`, fixes [#3941](https://github.com/grafana/grafana/issues/3941)
* **Row repeat**: Repeated rows will now appear next to each other and not by the bottom of the dashboard, fixes [#3942](https://github.com/grafana/grafana/issues/3942)
# 2.6.1 (unrelased, 2.6.x branch)

View File

@ -77,7 +77,7 @@ The Query Editor exposes capabilities of your Data Source and allows you to quer
Use the Query Editor to build one or more queries (for one or more series) in your time series database. The panel will instantly update allowing you to effectively explore your data in real time and build a perfect query for that particular Panel.
You can utilize [Template variables]((reference/templating/) in the Query Editor within the queries themselves. This provides a powerful way to explore data dynamically based on the Templating variables selected on the Dashboard.
You can utilize [Template variables](/reference/templating/) in the Query Editor within the queries themselves. This provides a powerful way to explore data dynamically based on the Templating variables selected on the Dashboard.
Grafana allows you to reference queries in the Query Editor by the row that theyre on. If you add a second query to graph, you can reference the first query simply by typing in #A. This provides an easy and convenient way to build compounded queries.

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ To view table panels in action and test different configurations with sample dat
The table panel has many ways to manipulate your data for optimal presentation.
<img class="no-shadow" src="/img/v2/table-config.png">
<img class="no-shadow" src="/img/v2/table-config2.png">
1. `Data`: Control how your query is transformed into a table.
2. `Table Display`: Table display options.
@ -33,19 +33,19 @@ you want in the table. Only applicable for some transforms.
### Time series to rows
<img src="/img/v2/table_ts_to_rows.png">
<img src="/img/v2/table_ts_to_rows2.png">
In the most simple mode you can turn time series to rows. This means you get a `Time`, `Metric` and a `Value` column. Where `Metric` is the name of the time series.
### Time series to columns
![](/img/v2/table_ts_to_columns.png)
![](/img/v2/table_ts_to_columns2.png)
This transform allows you to take multiple time series and group them by time. Which will result in the primary column being `Time` and a column for each time series.
### Time series aggregations
![](/img/v2/table_ts_to_aggregations.png)
![](/img/v2/table_ts_to_aggregations2.png)
This table transformation will lay out your table into rows by metric, allowing columns of `Avg`, `Min`, `Max`, `Total`, `Current` and `Count`. More than one column can be added.
### Annotations

View File

@ -60,7 +60,7 @@
"scripts": {
"test": "grunt test",
"coveralls": "grunt karma:coveralls && rm -rf ./coverage",
"postinstall": "grunt copy:node_modules"
"postinstall": "./node_modules/.bin/grunt copy:node_modules"
},
"license": "Apache-2.0",
"dependencies": {

View File

@ -69,9 +69,11 @@ func Register(r *macaron.Macaron) {
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), wrap(ResetPassword))
// dashboard snapshots
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
r.Get("/dashboard/snapshot/*", Index)
r.Get("/dashboard/snapshots/", reqSignedIn, Index)
// api for dashboard snapshots
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
r.Get("/api/snapshot/shared-options/", GetSharingOptions)
r.Get("/api/snapshots/:key", GetDashboardSnapshot)
r.Get("/api/snapshots-delete/:key", DeleteDashboardSnapshot)
@ -183,6 +185,11 @@ func Register(r *macaron.Macaron) {
r.Get("/tags", GetDashboardTags)
})
// Dashboard snapshots
r.Group("/dashboard/snapshots", func() {
r.Get("/", wrap(SearchDashboardSnapshots))
})
// Playlist
r.Group("/playlists", func() {
r.Get("/", wrap(SearchPlaylists))

View File

@ -3,7 +3,14 @@ package cloudwatch
import (
"encoding/json"
"sort"
"strings"
"sync"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awsutil"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/util"
)
@ -11,6 +18,14 @@ import (
var metricsMap map[string][]string
var dimensionsMap map[string][]string
type CustomMetricsCache struct {
Expire time.Time
Cache []string
}
var customMetricsMetricsMap map[string]map[string]map[string]*CustomMetricsCache
var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCache
func init() {
metricsMap = map[string][]string{
"AWS/AutoScaling": {"GroupMinSize", "GroupMaxSize", "GroupDesiredCapacity", "GroupInServiceInstances", "GroupPendingInstances", "GroupStandbyInstances", "GroupTerminatingInstances", "GroupTotalInstances"},
@ -85,6 +100,9 @@ func init() {
"AWS/WAF": {"Rule", "WebACL"},
"AWS/WorkSpaces": {"DirectoryId", "WorkspaceId"},
}
customMetricsMetricsMap = make(map[string]map[string]map[string]*CustomMetricsCache)
customMetricsDimensionsMap = make(map[string]map[string]map[string]*CustomMetricsCache)
}
// Whenever this list is updated, frontend list should also be updated.
@ -127,10 +145,19 @@ func handleGetMetrics(req *cwRequest, c *middleware.Context) {
json.Unmarshal(req.Body, reqParam)
namespaceMetrics, exists := metricsMap[reqParam.Parameters.Namespace]
if !exists {
c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
return
var namespaceMetrics []string
if !isCustomMetrics(reqParam.Parameters.Namespace) {
var exists bool
if namespaceMetrics, exists = metricsMap[reqParam.Parameters.Namespace]; !exists {
c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
return
}
} else {
var err error
if namespaceMetrics, err = getMetricsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, getAllMetrics); err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
}
sort.Sort(sort.StringSlice(namespaceMetrics))
@ -151,10 +178,19 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
json.Unmarshal(req.Body, reqParam)
dimensionValues, exists := dimensionsMap[reqParam.Parameters.Namespace]
if !exists {
c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
return
var dimensionValues []string
if !isCustomMetrics(reqParam.Parameters.Namespace) {
var exists bool
if dimensionValues, exists = dimensionsMap[reqParam.Parameters.Namespace]; !exists {
c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
return
}
} else {
var err error
if dimensionValues, err = getDimensionsForCustomMetrics(req.Region, reqParam.Parameters.Namespace, req.DataSource.Database, getAllMetrics); err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
}
sort.Sort(sort.StringSlice(dimensionValues))
@ -165,3 +201,122 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
c.JSON(200, result)
}
func getAllMetrics(region string, namespace string, database string) (cloudwatch.ListMetricsOutput, error) {
cfg := &aws.Config{
Region: aws.String(region),
Credentials: getCredentials(database),
}
svc := cloudwatch.New(session.New(cfg), cfg)
params := &cloudwatch.ListMetricsInput{
Namespace: aws.String(namespace),
}
var resp cloudwatch.ListMetricsOutput
err := svc.ListMetricsPages(params,
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
for _, metric := range metrics {
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
}
return !lastPage
})
if err != nil {
return resp, err
}
return resp, nil
}
var metricsCacheLock sync.Mutex
func getMetricsForCustomMetrics(region string, namespace string, database string, getAllMetrics func(string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(region, namespace, database)
if err != nil {
return []string{}, err
}
metricsCacheLock.Lock()
defer metricsCacheLock.Unlock()
if _, ok := customMetricsMetricsMap[database]; !ok {
customMetricsMetricsMap[database] = make(map[string]map[string]*CustomMetricsCache)
}
if _, ok := customMetricsMetricsMap[database][region]; !ok {
customMetricsMetricsMap[database][region] = make(map[string]*CustomMetricsCache)
}
if _, ok := customMetricsMetricsMap[database][region][namespace]; !ok {
customMetricsMetricsMap[database][region][namespace] = &CustomMetricsCache{}
customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0)
}
if customMetricsMetricsMap[database][region][namespace].Expire.After(time.Now()) {
return customMetricsMetricsMap[database][region][namespace].Cache, nil
}
customMetricsMetricsMap[database][region][namespace].Cache = make([]string, 0)
customMetricsMetricsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute)
for _, metric := range result.Metrics {
if isDuplicate(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName) {
continue
}
customMetricsMetricsMap[database][region][namespace].Cache = append(customMetricsMetricsMap[database][region][namespace].Cache, *metric.MetricName)
}
return customMetricsMetricsMap[database][region][namespace].Cache, nil
}
var dimensionsCacheLock sync.Mutex
func getDimensionsForCustomMetrics(region string, namespace string, database string, getAllMetrics func(string, string, string) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
result, err := getAllMetrics(region, namespace, database)
if err != nil {
return []string{}, err
}
dimensionsCacheLock.Lock()
defer dimensionsCacheLock.Unlock()
if _, ok := customMetricsDimensionsMap[database]; !ok {
customMetricsDimensionsMap[database] = make(map[string]map[string]*CustomMetricsCache)
}
if _, ok := customMetricsDimensionsMap[database][region]; !ok {
customMetricsDimensionsMap[database][region] = make(map[string]*CustomMetricsCache)
}
if _, ok := customMetricsDimensionsMap[database][region][namespace]; !ok {
customMetricsDimensionsMap[database][region][namespace] = &CustomMetricsCache{}
customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0)
}
if customMetricsDimensionsMap[database][region][namespace].Expire.After(time.Now()) {
return customMetricsDimensionsMap[database][region][namespace].Cache, nil
}
customMetricsDimensionsMap[database][region][namespace].Cache = make([]string, 0)
customMetricsDimensionsMap[database][region][namespace].Expire = time.Now().Add(5 * time.Minute)
for _, metric := range result.Metrics {
for _, dimension := range metric.Dimensions {
if isDuplicate(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name) {
continue
}
customMetricsDimensionsMap[database][region][namespace].Cache = append(customMetricsDimensionsMap[database][region][namespace].Cache, *dimension.Name)
}
}
return customMetricsDimensionsMap[database][region][namespace].Cache, nil
}
func isDuplicate(nameList []string, target string) bool {
for _, name := range nameList {
if name == target {
return true
}
}
return false
}
func isCustomMetrics(namespace string) bool {
return strings.Index(namespace, "AWS/") != 0
}

View File

@ -0,0 +1,63 @@
package cloudwatch
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
. "github.com/smartystreets/goconvey/convey"
)
func TestCloudWatchMetrics(t *testing.T) {
Convey("When calling getMetricsForCustomMetrics", t, func() {
region := "us-east-1"
namespace := "Foo"
database := "default"
f := func(region string, namespace string, database string) (cloudwatch.ListMetricsOutput, error) {
return cloudwatch.ListMetricsOutput{
Metrics: []*cloudwatch.Metric{
{
MetricName: aws.String("Test_MetricName"),
Dimensions: []*cloudwatch.Dimension{
{
Name: aws.String("Test_DimensionName"),
},
},
},
},
}, nil
}
metrics, _ := getMetricsForCustomMetrics(region, namespace, database, f)
Convey("Should contain Test_MetricName", func() {
So(metrics, ShouldContain, "Test_MetricName")
})
})
Convey("When calling getDimensionsForCustomMetrics", t, func() {
region := "us-east-1"
namespace := "Foo"
database := "default"
f := func(region string, namespace string, database string) (cloudwatch.ListMetricsOutput, error) {
return cloudwatch.ListMetricsOutput{
Metrics: []*cloudwatch.Metric{
{
MetricName: aws.String("Test_MetricName"),
Dimensions: []*cloudwatch.Dimension{
{
Name: aws.String("Test_DimensionName"),
},
},
},
},
}, nil
}
dimensionKeys, _ := getDimensionsForCustomMetrics(region, namespace, database, f)
Convey("Should contain Test_DimensionName", func() {
So(dimensionKeys, ShouldContain, "Test_DimensionName")
})
})
}

View File

@ -49,17 +49,13 @@ func GetDashboard(c *middleware.Context) {
dash := query.Result
// Finding the last updater of the dashboard
updater := "Anonymous"
if dash.UpdatedBy != 0 {
userQuery := m.GetUserByIdQuery{Id: dash.UpdatedBy}
userErr := bus.Dispatch(&userQuery)
if userErr != nil {
updater = "Unknown"
} else {
user := userQuery.Result
updater = user.Login
}
// Finding creator and last updater of the dashboard
updater, creator := "Anonymous", "Anonymous"
if dash.UpdatedBy > 0 {
updater = getUserLogin(dash.UpdatedBy)
}
if dash.CreatedBy > 0 {
creator = getUserLogin(dash.CreatedBy)
}
dto := dtos.DashboardFullWithMeta{
@ -74,12 +70,25 @@ func GetDashboard(c *middleware.Context) {
Created: dash.Created,
Updated: dash.Updated,
UpdatedBy: updater,
CreatedBy: creator,
Version: dash.Version,
},
}
c.JSON(200, dto)
}
func getUserLogin(userId int64) string {
query := m.GetUserByIdQuery{Id: userId}
err := bus.Dispatch(&query)
if err != nil {
return "Anonymous"
} else {
user := query.Result
return user.Login
}
}
func DeleteDashboard(c *middleware.Context) {
slug := c.Params(":slug")
@ -104,9 +113,9 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
cmd.OrgId = c.OrgId
if !c.IsSignedIn {
cmd.UpdatedBy = 0
cmd.UserId = -1
} else {
cmd.UpdatedBy = c.UserId
cmd.UserId = c.UserId
}
dash := cmd.GetDashboardModel()

View File

@ -36,7 +36,6 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho
cmd.DeleteKey = util.GetRandomString(32)
cmd.OrgId = c.OrgId
cmd.UserId = c.UserId
cmd.Name = c.Name
metrics.M_Api_Dashboard_Snapshot_Create.Inc(1)
}
@ -99,3 +98,43 @@ func DeleteDashboardSnapshot(c *middleware.Context) {
c.JSON(200, util.DynMap{"message": "Snapshot deleted. It might take an hour before it's cleared from a CDN cache."})
}
func SearchDashboardSnapshots(c *middleware.Context) Response {
query := c.Query("query")
limit := c.QueryInt("limit")
if limit == 0 {
limit = 1000
}
searchQuery := m.GetDashboardSnapshotsQuery{
Name: query,
Limit: limit,
OrgId: c.OrgId,
}
err := bus.Dispatch(&searchQuery)
if err != nil {
return ApiError(500, "Search failed", err)
}
dtos := make([]*m.DashboardSnapshotDTO, len(searchQuery.Result))
for i, snapshot := range searchQuery.Result {
dtos[i] = &m.DashboardSnapshotDTO{
Id: snapshot.Id,
Name: snapshot.Name,
Key: snapshot.Key,
DeleteKey: snapshot.DeleteKey,
OrgId: snapshot.OrgId,
UserId: snapshot.UserId,
External: snapshot.External,
ExternalUrl: snapshot.ExternalUrl,
Expires: snapshot.Expires,
Created: snapshot.Created,
Updated: snapshot.Updated,
}
}
return Json(200, dtos)
//return Json(200, searchQuery.Result)
}

View File

@ -42,6 +42,8 @@ type DashboardMeta struct {
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
UpdatedBy string `json:"updatedBy"`
CreatedBy string `json:"createdBy"`
Version int `json:"version"`
}
type DashboardFullWithMeta struct {

View File

@ -60,6 +60,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
Url: "/playlists",
})
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Snapshots",
Icon: "fa-fw icon-gf icon-gf-snapshot",
Url: "/dashboard/snapshots",
})
if c.OrgRole == m.ROLE_ADMIN {
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
Text: "Data Sources",

View File

@ -20,6 +20,22 @@ type DashboardSnapshot struct {
Dashboard map[string]interface{}
}
// DashboardSnapshotDTO without dashboard map
type DashboardSnapshotDTO struct {
Id int64 `json:"id"`
Name string `json:"name"`
Key string `json:"key"`
DeleteKey string `json:"deleteKey"`
OrgId int64 `json:"orgId"`
UserId int64 `json:"userId"`
External bool `json:"external"`
ExternalUrl string `json:"externalUrl"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// -----------------
// COMMANDS
@ -48,3 +64,13 @@ type GetDashboardSnapshotQuery struct {
Result *DashboardSnapshot
}
type DashboardSnapshots []*DashboardSnapshot
type GetDashboardSnapshotsQuery struct {
Name string
Limit int
OrgId int64
Result DashboardSnapshots
}

View File

@ -34,6 +34,7 @@ type Dashboard struct {
Updated time.Time
UpdatedBy int64
CreatedBy int64
Title string
Data map[string]interface{}
@ -91,8 +92,11 @@ func NewDashboardFromJson(data map[string]interface{}) *Dashboard {
// GetDashboardModel turns the command into the savable model
func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard {
dash := NewDashboardFromJson(cmd.Dashboard)
if dash.Data["version"] == 0 {
dash.CreatedBy = cmd.UserId
}
dash.UpdatedBy = cmd.UserId
dash.OrgId = cmd.OrgId
dash.UpdatedBy = cmd.UpdatedBy
dash.UpdateSlug()
return dash
}
@ -114,9 +118,9 @@ func (dash *Dashboard) UpdateSlug() {
type SaveDashboardCommand struct {
Dashboard map[string]interface{} `json:"dashboard" binding:"Required"`
Overwrite bool `json:"overwrite"`
UserId int64 `json:"userId"`
OrgId int64 `json:"-"`
UpdatedBy int64 `json:"-"`
Overwrite bool `json:"overwrite"`
Result *Dashboard
}

View File

@ -12,6 +12,7 @@ func init() {
bus.AddHandler("sql", CreateDashboardSnapshot)
bus.AddHandler("sql", GetDashboardSnapshot)
bus.AddHandler("sql", DeleteDashboardSnapshot)
bus.AddHandler("sql", SearchDashboardSnapshots)
}
func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
@ -64,3 +65,18 @@ func GetDashboardSnapshot(query *m.GetDashboardSnapshotQuery) error {
query.Result = &snapshot
return nil
}
func SearchDashboardSnapshots(query *m.GetDashboardSnapshotsQuery) error {
var snapshots = make(m.DashboardSnapshots, 0)
sess := x.Limit(query.Limit)
if query.Name != "" {
sess.Where("name LIKE ?", query.Name)
}
sess.Where("org_id = ?", query.OrgId)
err := sess.Find(&snapshots)
query.Result = snapshots
return err
}

View File

@ -97,4 +97,9 @@ func addDashboardMigration(mg *Migrator) {
mg.AddMigration("Add column updated_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
Name: "updated_by", Type: DB_Int, Nullable: true,
}))
// add column to store creator of a dashboard
mg.AddMigration("Add column created_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
Name: "created_by", Type: DB_Int, Nullable: true,
}))
}

View File

@ -3,7 +3,6 @@
import "./directives/annotation_tooltip";
import "./directives/body_class";
import "./directives/config_modal";
import "./directives/confirm_click";
import "./directives/dash_edit_link";
import "./directives/dash_upload";
@ -16,6 +15,8 @@ import "./directives/password_strenght";
import "./directives/spectrum_picker";
import "./directives/tags";
import "./directives/value_select_dropdown";
import "./directives/plugin_component";
import "./directives/rebuild_on_change";
import "./directives/give_focus";
import './jquery_extended';
import './partials';

View File

@ -1,46 +0,0 @@
define([
'lodash',
'jquery',
'../core_module',
],
function (_, $, coreModule) {
'use strict';
coreModule.default.directive('configModal', function($modal, $q, $timeout) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
var partial = attrs.configModal;
var id = '#' + partial.replace('.html', '').replace(/[\/|\.|:]/g, '-') + '-' + scope.$id;
elem.bind('click',function() {
if ($(id).length) {
elem.attr('data-target', id).attr('data-toggle', 'modal');
scope.$apply(function() { scope.$broadcast('modal-opened'); });
return;
}
var panelModal = $modal({
template: partial,
persist: false,
show: false,
scope: scope.$new(),
keyboard: false
});
$q.when(panelModal).then(function(modalEl) {
elem.attr('data-target', id).attr('data-toggle', 'modal');
$timeout(function () {
if (!modalEl.data('modal').isShown) {
modalEl.modal('show');
}
}, 50);
});
scope.$apply();
});
}
};
});
});

View File

@ -90,7 +90,6 @@ function (angular, coreModule, kbn) {
var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
'<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
(item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
(item.configModal ? ' dash-editor-link="' + item.configModal + '"' : "") +
'>' + (item.text || '') + '</a>';
if (item.submenu && item.submenu.length) {

View File

@ -0,0 +1,199 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import config from 'app/core/config';
import coreModule from 'app/core/core_module';
import {UnknownPanelCtrl} from 'app/plugins/panel/unknown/module';
/** @ngInject */
function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
function getTemplate(component) {
if (component.template) {
return $q.when(component.template);
}
var cached = $templateCache.get(component.templateUrl);
if (cached) {
return $q.when(cached);
}
return $http.get(component.templateUrl).then(res => {
return res.data;
});
}
function getPluginComponentDirective(options) {
return function() {
return {
templateUrl: options.Component.templateUrl,
template: options.Component.template,
restrict: 'E',
controller: options.Component,
controllerAs: 'ctrl',
bindToController: true,
scope: options.bindings,
link: (scope, elem, attrs, ctrl) => {
if (ctrl.link) {
ctrl.link(scope, elem, attrs, ctrl);
}
if (ctrl.init) {
ctrl.init();
}
}
};
};
}
function loadPanelComponentInfo(scope, attrs) {
var componentInfo: any = {
name: 'panel-plugin-' + scope.panel.type,
bindings: {dashboard: "=", panel: "=", row: "="},
attrs: {dashboard: "dashboard", panel: "panel", row: "row"},
};
var panelElemName = 'panel-' + scope.panel.type;
let panelInfo = config.panels[scope.panel.type];
var panelCtrlPromise = Promise.resolve(UnknownPanelCtrl);
if (panelInfo) {
panelCtrlPromise = System.import(panelInfo.module).then(function(panelModule) {
return panelModule.PanelCtrl;
});
}
return panelCtrlPromise.then(function(PanelCtrl: any) {
componentInfo.Component = PanelCtrl;
if (!PanelCtrl || PanelCtrl.registered) {
return componentInfo;
};
if (PanelCtrl.templatePromise) {
return PanelCtrl.templatePromise.then(res => {
return componentInfo;
});
}
PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => {
PanelCtrl.templateUrl = null;
PanelCtrl.template = `<grafana-panel ctrl="ctrl">${template}</grafana-panel>`;
return componentInfo;
});
return PanelCtrl.templatePromise;
});
}
function getModule(scope, attrs) {
switch (attrs.type) {
// QueryCtrl
case "query-ctrl": {
let datasource = scope.target.datasource || scope.ctrl.panel.datasource;
return datasourceSrv.get(datasource).then(ds => {
scope.datasource = ds;
return System.import(ds.meta.module).then(dsModule => {
return {
name: 'query-ctrl-' + ds.meta.id,
bindings: {target: "=", panelCtrl: "=", datasource: "="},
attrs: {"target": "target", "panel-ctrl": "ctrl", datasource: "datasource"},
Component: dsModule.QueryCtrl
};
});
});
}
// QueryOptionsCtrl
case "query-options-ctrl": {
return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
return System.import(ds.meta.module).then((dsModule): any => {
if (!dsModule.QueryOptionsCtrl) {
return {notFound: true};
}
return {
name: 'query-options-ctrl-' + ds.meta.id,
bindings: {panelCtrl: "="},
attrs: {"panel-ctrl": "ctrl"},
Component: dsModule.QueryOptionsCtrl
};
});
});
}
// Annotations
case "annotations-query-ctrl": {
return System.import(scope.currentDatasource.meta.module).then(function(dsModule) {
return {
name: 'annotations-query-ctrl-' + scope.currentDatasource.meta.id,
bindings: {annotation: "=", datasource: "="},
attrs: {"annotation": "currentAnnotation", datasource: "currentDatasource"},
Component: dsModule.AnnotationsQueryCtrl,
};
});
}
// ConfigCtrl
case 'datasource-config-ctrl': {
return System.import(scope.datasourceMeta.module).then(function(dsModule) {
return {
name: 'ds-config-' + scope.datasourceMeta.id,
bindings: {meta: "=", current: "="},
attrs: {meta: "datasourceMeta", current: "current"},
Component: dsModule.ConfigCtrl,
};
});
}
// Panel
case 'panel': {
return loadPanelComponentInfo(scope, attrs);
}
default: {
return $q.reject({message: "Could not find component type: " + attrs.type });
}
}
}
function appendAndCompile(scope, elem, componentInfo) {
var child = angular.element(document.createElement(componentInfo.name));
_.each(componentInfo.attrs, (value, key) => {
child.attr(key, value);
});
$compile(child)(scope);
elem.empty();
elem.append(child);
}
function registerPluginComponent(scope, elem, attrs, componentInfo) {
if (componentInfo.notFound) {
elem.empty();
return;
}
if (!componentInfo.Component) {
throw {message: 'Failed to find exported plugin component for ' + componentInfo.name};
}
if (!componentInfo.Component.registered) {
var directiveName = attrs.$normalize(componentInfo.name);
var directiveFn = getPluginComponentDirective(componentInfo);
coreModule.directive(directiveName, directiveFn);
componentInfo.Component.registered = true;
}
appendAndCompile(scope, elem, componentInfo);
}
return {
restrict: 'E',
link: function(scope, elem, attrs) {
getModule(scope, attrs).then(function (componentInfo) {
registerPluginComponent(scope, elem, attrs, componentInfo);
}).catch(err => {
$rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]);
console.log('Plugin componnet error', err);
});
}
};
}
coreModule.directive('pluginComponent', pluginDirectiveLoader);

View File

@ -0,0 +1,75 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../core_module';
function getBlockNodes(nodes) {
var node = nodes[0];
var endNode = nodes[nodes.length - 1];
var blockNodes;
for (var i = 1; node !== endNode && (node = node.nextSibling); i++) {
if (blockNodes || nodes[i] !== node) {
if (!blockNodes) {
blockNodes = $([].slice.call(nodes, 0, i));
}
blockNodes.push(node);
}
}
return blockNodes || nodes;
}
function rebuildOnChange($animate) {
return {
multiElement: true,
terminal: true,
transclude: true,
priority: 600,
restrict: 'E',
link: function(scope, elem, attrs, ctrl, transclude) {
var block, childScope, previousElements;
function cleanUp() {
if (previousElements) {
previousElements.remove();
previousElements = null;
}
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
previousElements = getBlockNodes(block.clone);
$animate.leave(previousElements).then(function() {
previousElements = null;
});
block = null;
}
}
scope.$watch(attrs.property, function rebuildOnChangeAction(value, oldValue) {
if (childScope && value !== oldValue) {
cleanUp();
}
if (!childScope && (value || attrs.showNull)) {
transclude(function(clone, newScope) {
childScope = newScope;
clone[clone.length++] = document.createComment(' end rebuild on change ');
block = {clone: clone};
$animate.enter(clone, elem.parent(), elem);
});
} else {
cleanUp();
}
});
}
};
}
coreModule.directive('rebuildOnChange', rebuildOnChange);

View File

View File

@ -137,6 +137,11 @@ define([
templateUrl: 'public/app/partials/reset_password.html',
controller : 'ResetPasswordCtrl',
})
.when('/dashboard/snapshots', {
templateUrl: 'public/app/features/snapshot/partials/snapshots.html',
controller : 'SnapshotsCtrl',
controllerAs: 'ctrl',
})
.when('/apps', {
templateUrl: 'public/app/features/apps/partials/list.html',
controller: 'AppListCtrl',

View File

@ -5,6 +5,7 @@ define([
'./templating/templateSrv',
'./dashboard/all',
'./playlist/all',
'./snapshot/all',
'./panel/all',
'./profile/profileCtrl',
'./profile/changePasswordCtrl',

View File

@ -2,7 +2,6 @@ define([
'angular',
'lodash',
'./editor_ctrl',
'./query_editor'
], function (angular, _) {
'use strict';

View File

@ -91,8 +91,10 @@
</div>
</div>
<annotations-query-editor datasource="currentDatasource" annotation="currentAnnotation">
</annotations-query-editor>
<rebuild-on-change property="currentAnnotation.datasource">
<plugin-component type="annotations-query-ctrl">
</plugin-component>
</rebuild-on-change>
<br>
<button ng-show="mode === 'new'" type="button" class="btn btn-success" ng-click="add()">Add</button>

View File

@ -1,25 +0,0 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function annotationsQueryEditor(dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
scope: {
annotation: "=",
datasource: "="
},
watchPath: "annotation.datasource",
directive: scope => {
return System.import(scope.datasource.meta.module).then(function(dsModule) {
return {
name: 'annotation-query-editor-' + scope.datasource.meta.id,
fn: dsModule.annotationsQueryEditor,
};
});
},
});
}
angular.module('grafana.directives').directive('annotationsQueryEditor', annotationsQueryEditor);

View File

@ -177,42 +177,6 @@ function (angular, $, _, moment) {
return newPanel;
};
p.getNextQueryLetter = function(panel) {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, function(refId) {
return _.every(panel.targets, function(other) {
return other.refId !== refId;
});
});
};
p.addDataQueryTo = function(panel, datasource) {
var target = {
refId: this.getNextQueryLetter(panel)
};
if (datasource) {
target.datasource = datasource.name;
}
panel.targets.push(target);
};
p.removeDataQuery = function (panel, query) {
panel.targets = _.without(panel.targets, query);
};
p.duplicateDataQuery = function(panel, query) {
var clone = angular.copy(query);
clone.refId = this.getNextQueryLetter(panel);
panel.targets.push(clone);
};
p.moveDataQuery = function(panel, fromIndex, toIndex) {
_.move(panel.targets, fromIndex, toIndex);
};
p.formatDate = function(date, format) {
date = moment.isMoment(date) ? date : moment(date);
format = format || 'YYYY-MM-DD HH:mm:ss';
@ -230,11 +194,21 @@ function (angular, $, _, moment) {
moment.utc(date).fromNow();
};
p.getNextQueryLetter = function(panel) {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, function(refId) {
return _.every(panel.targets, function(other) {
return other.refId !== refId;
});
});
};
p._updateSchema = function(old) {
var i, j, k;
var oldVersion = this.schemaVersion;
var panelUpgrades = [];
this.schemaVersion = 9;
this.schemaVersion = 10;
if (oldVersion === this.schemaVersion) {
return;
@ -407,6 +381,22 @@ function (angular, $, _, moment) {
});
}
// schema version 10 changes
if (oldVersion < 10) {
// move aliasYAxis changes
panelUpgrades.push(function(panel) {
if (panel.type !== 'table') { return; }
_.each(panel.styles, function(style) {
if (style.thresholds && style.thresholds.length >= 3) {
var k = style.thresholds;
k.shift();
style.thresholds = k;
}
});
});
}
if (panelUpgrades.length === 0) {
return;
}

View File

@ -34,7 +34,7 @@ function (angular, _) {
// handle row repeats
if (row.repeat) {
this.repeatRow(row);
this.repeatRow(row, i);
}
// clean up old left overs
else if (row.repeatRowId && row.repeatIteration !== this.iteration) {
@ -58,13 +58,13 @@ function (angular, _) {
};
// returns a new row clone or reuses a clone from previous iteration
this.getRowClone = function(sourceRow, index) {
if (index === 0) {
this.getRowClone = function(sourceRow, repeatIndex, sourceRowIndex) {
if (repeatIndex === 0) {
return sourceRow;
}
var i, panel, row, copy;
var sourceRowId = _.indexOf(this.dashboard.rows, sourceRow) + 1;
var sourceRowId = sourceRowIndex + 1;
// look for row to reuse
for (i = 0; i < this.dashboard.rows.length; i++) {
@ -77,7 +77,7 @@ function (angular, _) {
if (!copy) {
copy = angular.copy(sourceRow);
this.dashboard.rows.push(copy);
this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy);
// set new panel ids
for (i = 0; i < copy.panels.length; i++) {
@ -92,8 +92,8 @@ function (angular, _) {
return copy;
};
// returns a new panel clone or reuses a clone from previous iteration
this.repeatRow = function(row) {
// returns a new row clone or reuses a clone from previous iteration
this.repeatRow = function(row, rowIndex) {
var variables = this.dashboard.templating.list;
var variable = _.findWhere(variables, {name: row.repeat});
if (!variable) {
@ -108,7 +108,7 @@ function (angular, _) {
}
_.each(selected, function(option, index) {
copy = self.getRowClone(row, index);
copy = self.getRowClone(row, index, rowIndex);
copy.scopedVars = {};
copy.scopedVars[variable.name] = option;

View File

@ -115,9 +115,9 @@
</div>
<div ng-if="editor.index == 4">
<div class="editor-row">
<div class="tight-form-section">
<h5>Dashboard info</h5>
<div class="row">
<h5>Dashboard info</h5>
<div class="pull-left tight-form">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px">
@ -130,17 +130,6 @@
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px">
Created at:
</li>
<li class="tight-form-item" style="width: 180px">
{{formatDate(dashboardMeta.created)}}
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px">
Last updated by:
@ -150,6 +139,39 @@
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px">
Created at:
</li>
<li class="tight-form-item" style="width: 180px">
{{formatDate(dashboardMeta.created)}}
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px">
Created by:
</li>
<li class="tight-form-item" style="width: 180px">
{{dashboardMeta.createdBy}}
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 120px">
Current version:
</li>
<li class="tight-form-item" style="width: 180px">
{{dashboardMeta.version}}
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@ -61,6 +61,13 @@ function (angular, _, config) {
});
};
$scope.editRow = function() {
$scope.appEvent('show-dash-editor', {
src: 'public/app/partials/roweditor.html',
scope: $scope.$new()
});
};
$scope.moveRow = function(direction) {
var rowsList = $scope.dashboard.rows;
var rowIndex = _.indexOf(rowsList, $scope.row);

View File

@ -103,6 +103,11 @@ function (angular, _, $) {
if (!panelScope) {
return;
}
if (!panelScope.ctrl.editModeInitiated) {
panelScope.ctrl.initEditMode();
}
this.enterFullscreen(panelScope);
return;
}

View File

@ -1,5 +1,4 @@
define([
'./list_ctrl',
'./edit_ctrl',
'./config_view',
], function () {});

View File

@ -1,25 +0,0 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function dsConfigView(dynamicDirectiveSrv) {
return dynamicDirectiveSrv.create({
scope: {
dsMeta: "=",
current: "="
},
watchPath: "dsMeta.module",
directive: scope => {
return System.import(scope.dsMeta.module).then(function(dsModule) {
return {
name: 'ds-config-' + scope.dsMeta.id,
fn: dsModule.configView,
};
});
},
});
}
angular.module('grafana.directives').directive('dsConfigView', dsConfigView);

View File

@ -10,7 +10,10 @@ function (angular, _, config) {
var datasourceTypes = [];
module.directive('datasourceHttpSettings', function() {
return {templateUrl: 'public/app/features/datasources/partials/http_settings.html'};
return {
scope: {current: "="},
templateUrl: 'public/app/features/datasources/partials/http_settings.html'
};
});
module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) {

View File

@ -41,7 +41,10 @@
<div class="clearfix"></div>
</div>
<ds-config-view ng-if="datasourceMeta.id" ds-meta="datasourceMeta" current="current"></ds-config-view>
<rebuild-on-change property="datasourceMeta.id">
<plugin-component type="datasource-config-ctrl">
</plugin-component>
</rebuild-on-change>
<div ng-if="testing" style="margin-top: 25px">
<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>

View File

@ -2,7 +2,7 @@ define([
'./panel_menu',
'./panel_directive',
'./solo_panel_ctrl',
'./panel_loader',
'./query_editor',
'./query_ctrl',
'./panel_editor_tab',
'./query_editor_row',
], function () {});

View File

@ -38,25 +38,15 @@ class MetricsPanelCtrl extends PanelCtrl {
if (!this.panel.targets) {
this.panel.targets = [{}];
}
// hookup initial data fetch
this.$timeout(() => {
if (!this.skipDataOnInit) {
this.refresh();
}
}, 30);;
}
initEditMode() {
super.initEditMode();
this.addEditorTab('Metrics', 'public/app/partials/metrics.html');
this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html');
this.datasources = this.datasourceSrv.getMetricSources();
}
refresh() {
this.getData();
}
refreshData(data) {
// null op
return this.$q.when(data);
@ -67,13 +57,14 @@ class MetricsPanelCtrl extends PanelCtrl {
return data;
}
getData() {
refresh() {
// ignore fetching data if another panel is in fullscreen
if (this.otherPanelInFullscreenMode()) { return; }
// if we have snapshot data use that
if (this.panel.snapshotData) {
if (this.loadSnapshot) {
this.updateTimeRange();
this.loadSnapshot(this.panel.snapshotData);
}
return;
@ -140,6 +131,7 @@ class MetricsPanelCtrl extends PanelCtrl {
this.rangeRaw.from = timeFromInfo.from;
this.rangeRaw.to = timeFromInfo.to;
this.range.from = timeFromDate;
this.range.to = dateMath.parse(timeFromInfo.to);
}
}
@ -164,12 +156,12 @@ class MetricsPanelCtrl extends PanelCtrl {
};
issueQueries(datasource) {
this.updateTimeRange();
if (!this.panel.targets || this.panel.targets.length === 0) {
return this.$q.when([]);
}
this.updateTimeRange();
var metricsQuery = {
range: this.range,
rangeRaw: this.rangeRaw,
@ -182,32 +174,19 @@ class MetricsPanelCtrl extends PanelCtrl {
};
this.setTimeQueryStart();
return datasource.query(metricsQuery).then(results => {
this.setTimeQueryEnd();
try {
return datasource.query(metricsQuery).then(results => {
this.setTimeQueryEnd();
if (this.dashboard.snapshot) {
this.panel.snapshotData = results;
}
if (this.dashboard.snapshot) {
this.panel.snapshotData = results;
}
return results;
});
}
addDataQuery(datasource) {
this.dashboard.addDataQueryTo(this.panel, datasource);
}
removeDataQuery(query) {
this.dashboard.removeDataQuery(this.panel, query);
this.refresh();
};
duplicateDataQuery(query) {
this.dashboard.duplicateDataQuery(this.panel, query);
}
moveDataQuery(fromIndex, toIndex) {
this.dashboard.moveDataQuery(this.panel, fromIndex, toIndex);
return results;
});
} catch (err) {
return this.$q.reject(err);
}
}
setDatasource(datasource) {
@ -229,6 +208,13 @@ class MetricsPanelCtrl extends PanelCtrl {
this.datasource = null;
this.refresh();
}
addDataQuery(datasource) {
var target = {
datasource: datasource ? datasource.name : undefined
};
this.panel.targets.push(target);
}
}
export {MetricsPanelCtrl};

View File

@ -4,48 +4,10 @@ import config from 'app/core/config';
import {PanelCtrl} from './panel_ctrl';
import {MetricsPanelCtrl} from './metrics_panel_ctrl';
export class DefaultPanelCtrl extends PanelCtrl {
/** @ngInject */
constructor($scope, $injector) {
super($scope, $injector);
}
}
class PanelDirective {
template: string;
templateUrl: string;
bindToController: boolean;
scope: any;
controller: any;
controllerAs: string;
getDirective() {
if (!this.controller) {
this.controller = DefaultPanelCtrl;
}
return {
template: this.template,
templateUrl: this.templateUrl,
controller: this.controller,
controllerAs: 'ctrl',
bindToController: true,
scope: {dashboard: "=", panel: "=", row: "="},
link: (scope, elem, attrs, ctrl) => {
ctrl.init();
this.link(scope, elem, attrs, ctrl);
}
};
}
link(scope, elem, attrs, ctrl) {
return null;
}
}
import {QueryCtrl} from './query_ctrl';
export {
PanelCtrl,
MetricsPanelCtrl,
PanelDirective,
QueryCtrl,
}

View File

@ -2,6 +2,7 @@
import config from 'app/core/config';
import _ from 'lodash';
import angular from 'angular';
export class PanelCtrl {
panel: any;
@ -63,12 +64,6 @@ export class PanelCtrl {
}
editPanel() {
if (!this.editModeInitiated) {
this.editorTabs = [];
this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
this.initEditMode();
}
this.changeView(true, true);
}
@ -77,7 +72,9 @@ export class PanelCtrl {
}
initEditMode() {
return;
this.editorTabs = [];
this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
this.editModeInitiated = true;
}
addEditorTab(title, directiveFn, index?) {
@ -166,14 +163,26 @@ export class PanelCtrl {
});
}
sharePanel() {
var shareScope = this.$scope.$new();
shareScope.panel = this.panel;
shareScope.dashboard = this.dashboard;
sharePanel() {
var shareScope = this.$scope.$new();
shareScope.panel = this.panel;
shareScope.dashboard = this.dashboard;
this.publishAppEvent('show-modal', {
this.publishAppEvent('show-modal', {
src: 'public/app/features/dashboard/partials/shareModal.html',
scope: shareScope
});
}
}
openInspector() {
var modalScope = this.$scope.$new();
modalScope.panel = this.panel;
modalScope.dashboard = this.dashboard;
modalScope.inspector = angular.copy(this.inspector);
this.publishAppEvent('show-modal', {
src: 'public/app/partials/inspector.html',
scope: modalScope
});
}
}

View File

@ -1,104 +0,0 @@
define([
'angular',
'jquery',
],
function (angular, $) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('grafanaPanel', function() {
return {
restrict: 'E',
templateUrl: 'public/app/features/panel/partials/panel.html',
transclude: true,
scope: { ctrl: "=" },
link: function(scope, elem) {
var panelContainer = elem.find('.panel-container');
var ctrl = scope.ctrl;
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() {
panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' });
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
});
}
};
});
module.directive('panelResizer', function($rootScope) {
return {
restrict: 'E',
template: '<span class="resize-panel-handle"></span>',
link: function(scope, elem) {
var resizing = false;
var lastPanel = false;
var ctrl = scope.ctrl;
var handleOffset;
var originalHeight;
var originalWidth;
var maxWidth;
function dragStartHandler(e) {
e.preventDefault();
resizing = true;
handleOffset = $(e.target).offset();
originalHeight = parseInt(ctrl.row.height);
originalWidth = ctrl.panel.span;
maxWidth = $(document).width();
lastPanel = ctrl.row.panels[ctrl.row.panels.length - 1];
$('body').on('mousemove', moveHandler);
$('body').on('mouseup', dragEndHandler);
}
function moveHandler(e) {
ctrl.row.height = originalHeight + (e.pageY - handleOffset.top);
ctrl.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
ctrl.panel.span = Math.min(Math.max(ctrl.panel.span, 1), 12);
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
// auto adjust other panels
if (Math.floor(rowSpan) < 14) {
// last panel should not push row down
if (lastPanel === ctrl.panel && rowSpan > 12) {
lastPanel.span -= rowSpan - 12;
}
// reduce width of last panel so total in row is 12
else if (lastPanel !== ctrl.panel) {
lastPanel.span = lastPanel.span - (rowSpan - 12);
lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12);
}
}
scope.$apply(function() {
scope.$broadcast('render');
});
}
function dragEndHandler() {
// if close to 12
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
if (rowSpan < 12 && rowSpan > 11) {
lastPanel.span += 12 - rowSpan;
}
scope.$apply(function() {
$rootScope.$broadcast('render');
});
$('body').off('mousemove', moveHandler);
$('body').off('mouseup', dragEndHandler);
}
elem.on('mousedown', dragStartHandler);
scope.$on("$destroy", function() {
elem.off('mousedown', dragStartHandler);
});
}
};
});
});

View File

@ -0,0 +1,101 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import $ from 'jquery';
var module = angular.module('grafana.directives');
module.directive('grafanaPanel', function() {
return {
restrict: 'E',
templateUrl: 'public/app/features/panel/partials/panel.html',
transclude: true,
scope: { ctrl: "=" },
link: function(scope, elem) {
var panelContainer = elem.find('.panel-container');
var ctrl = scope.ctrl;
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.height', 'ctrl.panel.height', 'ctrl.row.height'], function() {
panelContainer.css({ minHeight: ctrl.height || ctrl.panel.height || ctrl.row.height, display: 'block' });
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
});
}
};
});
module.directive('panelResizer', function($rootScope) {
return {
restrict: 'E',
template: '<span class="resize-panel-handle"></span>',
link: function(scope, elem) {
var resizing = false;
var lastPanel;
var ctrl = scope.ctrl;
var handleOffset;
var originalHeight;
var originalWidth;
var maxWidth;
function dragStartHandler(e) {
e.preventDefault();
resizing = true;
handleOffset = $(e.target).offset();
originalHeight = parseInt(ctrl.row.height);
originalWidth = ctrl.panel.span;
maxWidth = $(document).width();
lastPanel = ctrl.row.panels[ctrl.row.panels.length - 1];
$('body').on('mousemove', moveHandler);
$('body').on('mouseup', dragEndHandler);
}
function moveHandler(e) {
ctrl.row.height = originalHeight + (e.pageY - handleOffset.top);
ctrl.panel.span = originalWidth + (((e.pageX - handleOffset.left) / maxWidth) * 12);
ctrl.panel.span = Math.min(Math.max(ctrl.panel.span, 1), 12);
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
// auto adjust other panels
if (Math.floor(rowSpan) < 14) {
// last panel should not push row down
if (lastPanel === ctrl.panel && rowSpan > 12) {
lastPanel.span -= rowSpan - 12;
} else if (lastPanel !== ctrl.panel) {
// reduce width of last panel so total in row is 12
lastPanel.span = lastPanel.span - (rowSpan - 12);
lastPanel.span = Math.min(Math.max(lastPanel.span, 1), 12);
}
}
scope.$apply(function() {
scope.$broadcast('render');
});
}
function dragEndHandler() {
// if close to 12
var rowSpan = ctrl.dashboard.rowSpan(ctrl.row);
if (rowSpan < 12 && rowSpan > 11) {
lastPanel.span += 12 - rowSpan;
}
scope.$apply(function() {
$rootScope.$broadcast('render');
});
$('body').off('mousemove', moveHandler);
$('body').off('mouseup', dragEndHandler);
}
elem.on('mousedown', dragStartHandler);
scope.$on("$destroy", function() {
elem.off('mousedown', dragStartHandler);
});
}
};
});

View File

@ -1,88 +0,0 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import config from 'app/core/config';
import {UnknownPanel} from '../../plugins/panel/unknown/module';
var directiveModule = angular.module('grafana.directives');
/** @ngInject */
function panelLoader($compile, dynamicDirectiveSrv, $http, $q, $injector, $templateCache) {
return {
restrict: 'E',
scope: {
dashboard: "=",
row: "=",
panel: "="
},
link: function(scope, elem, attrs) {
function getTemplate(directive) {
if (directive.template) {
return $q.when(directive.template);
}
var cached = $templateCache.get(directive.templateUrl);
if (cached) {
return $q.when(cached);
}
return $http.get(directive.templateUrl).then(res => {
return res.data;
});
}
function addPanelAndCompile(name) {
var child = angular.element(document.createElement(name));
child.attr('dashboard', 'dashboard');
child.attr('panel', 'panel');
child.attr('row', 'row');
$compile(child)(scope);
elem.empty();
elem.append(child);
}
function addPanel(name, Panel) {
if (Panel.registered) {
addPanelAndCompile(name);
return;
}
if (Panel.promise) {
Panel.promise.then(() => {
addPanelAndCompile(name);
});
return;
}
var panelInstance = $injector.instantiate(Panel);
var directive = panelInstance.getDirective();
Panel.promise = getTemplate(directive).then(template => {
directive.templateUrl = null;
directive.template = `<grafana-panel ctrl="ctrl">${template}</grafana-panel>`;
directiveModule.directive(attrs.$normalize(name), function() {
return directive;
});
Panel.registered = true;
addPanelAndCompile(name);
});
}
var panelElemName = 'panel-directive-' + scope.panel.type;
let panelInfo = config.panels[scope.panel.type];
if (!panelInfo) {
addPanel(panelElemName, UnknownPanel);
return;
}
System.import(panelInfo.module).then(function(panelModule) {
addPanel(panelElemName, panelModule.Panel);
}).catch(err => {
console.log('Panel err: ', err);
});
}
};
}
directiveModule.directive('panelLoader', panelLoader);

View File

@ -37,7 +37,7 @@ function (angular, $, _) {
template += '<div class="panel-menu-row">';
template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
template += '<a class="panel-menu-icon pull-left" ng-click="ctrl.updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
template += '<a class="panel-menu-icon pull-right" ng-click="ctrl.removePanel()"><i class="fa fa-remove"></i></a>';
template += '<a class="panel-menu-icon pull-right" ng-click="ctrl.removePanel()"><i class="fa fa-trash"></i></a>';
template += '<div class="clearfix"></div>';
template += '</div>';
}
@ -53,7 +53,6 @@ function (angular, $, _) {
template += '<a class="panel-menu-link" ';
if (item.click) { template += ' ng-click="' + item.click + '"'; }
if (item.editorLink) { template += ' dash-editor-link="' + item.editorLink + '"'; }
template += '>';
template += item.text + '</a>';
});

View File

@ -1,6 +1,6 @@
<div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
<div class="panel-header">
<span class="alert-error panel-error small pointer" config-modal="app/partials/inspector.html" ng-if="ctrl.error">
<span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
<span data-placement="top" bs-tooltip="ctrl.error">
<i class="fa fa-exclamation"></i><span class="panel-error-arrow"></span>
</span>

View File

@ -0,0 +1,56 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="ctrl.error" class="tight-form-item">
<a bs-tooltip="ctrl.error" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="ctrl.target.datasource">
<em>{{ctrl.target.datasource}}</em>
</li>
<li class="tight-form-item" ng-if="ctrl.toggleEditorMode">
<a class="pointer" tabindex="1" ng-click="ctrl.toggleEditorMode()">
<i class="fa fa-pencil"></i>
</a>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a>
</li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(target)">
<i class="fa fa-trash"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{ctrl.target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="ctrl.toggleHideQuery()" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list" ng-transclude>
</ul>
<div class="clearfix"></div>
</div>

View File

@ -0,0 +1,57 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class QueryCtrl {
target: any;
datasource: any;
panelCtrl: any;
panel: any;
hasRawMode: boolean;
error: string;
constructor(public $scope, private $injector) {
this.panel = this.panelCtrl.panel;
if (!this.target.refId) {
this.target.refId = this.getNextQueryLetter();
}
}
getNextQueryLetter() {
var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
return _.find(letters, refId => {
return _.every(this.panel.targets, function(other) {
return other.refId !== refId;
});
});
}
removeQuery() {
this.panel.targets = _.without(this.panel.targets, this.target);
this.panelCtrl.refresh();
};
duplicateQuery() {
var clone = angular.copy(this.target);
clone.refId = this.getNextQueryLetter();
this.panel.targets.push(clone);
}
moveQuery(direction) {
var index = _.indexOf(this.panel.targets, this.target);
_.move(this.panel.targets, index, index + direction);
}
refresh() {
this.panelCtrl.refresh();
}
toggleHideQuery() {
this.target.hide = !this.target.hide;
this.panelCtrl.refresh();
}
}

View File

@ -1,48 +0,0 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
/** @ngInject */
function metricsQueryEditor(dynamicDirectiveSrv, datasourceSrv) {
return dynamicDirectiveSrv.create({
watchPath: "ctrl.panel.datasource",
directive: scope => {
let datasource = scope.target.datasource || scope.ctrl.panel.datasource;
return datasourceSrv.get(datasource).then(ds => {
scope.datasource = ds;
if (!scope.target.refId) {
scope.target.refId = 'A';
}
return System.import(ds.meta.module).then(dsModule => {
return {
name: 'metrics-query-editor-' + ds.meta.id,
fn: dsModule.metricsQueryEditor,
};
});
});
}
});
}
/** @ngInject */
function metricsQueryOptions(dynamicDirectiveSrv, datasourceSrv) {
return dynamicDirectiveSrv.create({
watchPath: "ctrl.panel.datasource",
directive: scope => {
return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
return System.import(ds.meta.module).then(dsModule => {
return {
name: 'metrics-query-options-' + ds.meta.id,
fn: dsModule.metricsQueryOptions
};
});
});
}
});
}
angular.module('grafana.directives')
.directive('metricsQueryEditor', metricsQueryEditor)
.directive('metricsQueryOptions', metricsQueryOptions);

View File

@ -0,0 +1,18 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import $ from 'jquery';
var module = angular.module('grafana.directives');
/** @ngInject **/
function queryEditorRowDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/features/panel/partials/query_editor_row.html',
transclude: true,
scope: {ctrl: "="},
};
}
module.directive('queryEditorRow', queryEditorRowDirective);

View File

@ -0,0 +1 @@
import './snapshot_ctrl';

View File

@ -0,0 +1,39 @@
<navbar icon="icon-gf icon-gf-snapshot" title="Dashboard snapshots"></navbar>
<div class="page-container">
<div class="page-wide">
<h2>Available snapshots</h2>
<table class="filter-table" style="margin-top: 20px">
<thead>
<th><strong>Name</strong></th>
<th><strong>Snapshot url</strong></th>
<th style="width: 70px"></th>
<th style="width: 25px"></th>
</thead>
<tr ng-repeat="snapshot in ctrl.snapshots">
<td>
<a href="dashboard/snapshot/{{snapshot.key}}">{{snapshot.name}}</a>
</td>
<td >
<a href="dashboard/snapshot/{{snapshot.key}}">dashboard/snapshot/{{snapshot.key}}</a>
</td>
<td class="text-center">
<a href="dashboard/snapshot/{{snapshot.key}}" class="btn btn-inverse btn-mini">
<i class="fa fa-eye"></i>
View
</a>
</td>
<td class="text-right">
<a ng-click="ctrl.removeSnapshot(snapshot)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</table>
</div>
</div>

View File

@ -0,0 +1,42 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class SnapshotsCtrl {
snapshots: any;
/** @ngInject */
constructor(private $rootScope, private backendSrv) {
this.backendSrv.get('/api/dashboard/snapshots').then(result => {
this.snapshots = result;
});
}
removeSnapshotConfirmed(snapshot) {
_.remove(this.snapshots, {key: snapshot.key});
this.backendSrv.get('/api/snapshots-delete/' + snapshot.deleteKey)
.then(() => {
this.$rootScope.appEvent('alert-success', ['Snapshot deleted', '']);
}, () => {
this.$rootScope.appEvent('alert-error', ['Unable to delete snapshot', '']);
this.snapshots.push(snapshot);
});
}
removeSnapshot(snapshot) {
this.$rootScope.appEvent('confirm-modal', {
title: 'Confirm delete snapshot',
text: 'Are you sure you want to delete snapshot ' + snapshot.name + '?',
yesText: "Delete",
icon: "fa-warning",
onConfirm: () => {
this.removeSnapshotConfirmed(snapshot);
}
});
}
}
angular.module('grafana.controllers').controller('SnapshotsCtrl', SnapshotsCtrl);

View File

@ -65,7 +65,7 @@
</ul>
</li>
<li>
<a dash-editor-link="app/partials/roweditor.html">Row editor</a>
<a ng-click="editRow()">Row editor</a>
</li>
<li>
<a ng-click="deleteRow()">Delete row</a>
@ -81,8 +81,8 @@
<div ng-repeat="panel in row.panels track by panel.id" class="panel" ui-draggable="!dashboard.meta.fullscreen" drag="panel.id"
ui-on-drop="onDrop($data, row, panel)" drag-handle-class="drag-handle" panel-width>
<panel-loader class="panel-margin" dashboard="dashboard" row="row" panel="panel">
</panel-loader>
<plugin-component type="panel" class="panel-margin">
</plugin-component>
</div>
<div panel-drop-zone class="panel panel-drop-zone" ui-on-drop="onDrop($data, row)" data-drop="true">

View File

@ -61,9 +61,9 @@
<div ng-if="editor.index == 2">
<label>Message:</label>
<pre>
{{message}}
</pre>
<pre>
{{message}}
</pre>
<label>Stack trace:</label>
<pre>

View File

@ -1,8 +1,12 @@
<div class="editor-row">
<div class="tight-form-container">
<metrics-query-editor ng-repeat="target in ctrl.panel.targets" ng-class="{'tight-form-disabled': target.hide}" >
</metrics-query-editor>
<div ng-repeat="target in ctrl.panel.targets" ng-class="{'tight-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>
<div style="margin: 20px 0 0 0">
@ -26,7 +30,11 @@
</div>
<metrics-query-options></metrics-query-options>
<rebuild-on-change property="ctrl.panel.datasource" show-null="true">
<plugin-component type="query-options-ctrl">
</plugin-component>
</rebuild-on-change>
</div>
<div class="editor-row" style="margin-top: 30px">

View File

@ -1,3 +1,3 @@
declare var Datasource: any;
export default Datasource;
declare var CloudWatchDatasource: any;
export {CloudWatchDatasource};

View File

@ -90,18 +90,20 @@ function (angular, _, moment, dateMath) {
return this.awsRequest({action: '__GetNamespaces'});
};
this.getMetrics = function(namespace) {
this.getMetrics = function(namespace, region) {
return this.awsRequest({
action: '__GetMetrics',
region: region,
parameters: {
namespace: templateSrv.replace(namespace)
}
});
};
this.getDimensionKeys = function(namespace) {
this.getDimensionKeys = function(namespace, region) {
return this.awsRequest({
action: '__GetDimensions',
region: region,
parameters: {
namespace: templateSrv.replace(namespace)
}
@ -164,14 +166,14 @@ function (angular, _, moment, dateMath) {
return this.getNamespaces();
}
var metricNameQuery = query.match(/^metrics\(([^\)]+?)\)/);
var metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
if (metricNameQuery) {
return this.getMetrics(metricNameQuery[1]);
return this.getMetrics(metricNameQuery[1], metricNameQuery[3]);
}
var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)\)/);
var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
if (dimensionKeysQuery) {
return this.getDimensionKeys(dimensionKeysQuery[1]);
return this.getDimensionKeys(dimensionKeysQuery[1], dimensionKeysQuery[3]);
}
var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/);
@ -357,5 +359,7 @@ function (angular, _, moment, dateMath) {
}
return CloudWatchDatasource;
return {
CloudWatchDatasource: CloudWatchDatasource
};
});

View File

@ -1,27 +0,0 @@
define([
'./datasource',
'./query_parameter_ctrl',
'./query_ctrl',
],
function (CloudWatchDatasource) {
'use strict';
function metricsQueryEditor() {
return {controller: 'CloudWatchQueryCtrl', templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/query.editor.html'};
}
function annotationsQueryEditor() {
return {templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html'};
}
function configView() {
return {templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/edit_view.html'};
}
return {
Datasource: CloudWatchDatasource,
configView: configView,
annotationsQueryEditor: annotationsQueryEditor,
metricsQueryEditor: metricsQueryEditor,
};
});

View File

@ -0,0 +1,20 @@
import './query_parameter_ctrl';
import {CloudWatchDatasource} from './datasource';
import {CloudWatchQueryCtrl} from './query_ctrl';
class CloudWatchConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/cloudwatch/partials/config.html';
}
class CloudWatchAnnotationsQueryCtrl {
static templateUrl = 'public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html';
}
export {
CloudWatchDatasource as Datasource,
CloudWatchQueryCtrl as QueryCtrl,
CloudWatchConfigCtrl as ConfigCtrl,
CloudWatchAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};

View File

@ -1 +1 @@
<cloudwatch-query-parameter target="annotation" datasource="datasource"></cloudwatch-query-parameter>
<cloudwatch-query-parameter target="ctrl.annotation" datasource="ctrl.datasource"></cloudwatch-query-parameter>

View File

@ -9,7 +9,7 @@
Credentials profile name<tip>Credentials profile name, as specified in ~/.aws/credentials, leave blank for default</tip>
</li>
<li>
<input type="text" class="tight-form-input input-large last" ng-model='current.database' placeholder="default"></input>
<input type="text" class="tight-form-input input-large last" ng-model='ctrl.current.database' placeholder="default"></input>
</li>
</ul>
<div class="clearfix"></div>
@ -19,12 +19,12 @@
<li class="tight-form-item" style="width: 200px">
Default Region<tip>Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.</tip>
</li>
<!--
<!--
Whenever this list is updated, backend list should also be updated.
Please update the region list in pkg/api/cloudwatch/metric.go
-->
<li>
<select class="tight-form-input input-large last" ng-model="current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-west-1', 'us-west-2']"></select>
<select class="tight-form-input input-large last" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-west-1', 'us-west-2']"></select>
</li>
</ul>
<div class="clearfix"></div>

View File

@ -1,38 +1,4 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="ctrl.duplicateDataQuery(target)">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index+1)">Move down</a></li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="ctrl.removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<query-editor-row ctrl="ctrl">
</query-editor-row>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item"
ng-click="target.hide = !target.hide; ctrl.refresh();"
role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<cloudwatch-query-parameter target="target" datasource="ctrl.datasource" on-change="refreshMetricData()"></cloudwatch-query-parameter>
<cloudwatch-query-parameter target="ctrl.target" datasource="ctrl.datasource" on-change="ctrl.refresh()"></cloudwatch-query-parameter>

View File

@ -1,27 +0,0 @@
define([
'angular',
'lodash',
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('CloudWatchQueryCtrl', function($scope) {
$scope.init = function() {
$scope.aliasSyntax = '{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}';
};
$scope.refreshMetricData = function() {
if (!_.isEqual($scope.oldTarget, $scope.target)) {
$scope.oldTarget = angular.copy($scope.target);
$scope.ctrl.refresh();
}
};
$scope.init();
});
});

View File

@ -0,0 +1,17 @@
///<reference path="../../../headers/common.d.ts" />
import './query_parameter_ctrl';
import _ from 'lodash';
import {QueryCtrl} from 'app/features/panel/panel';
export class CloudWatchQueryCtrl extends QueryCtrl {
static templateUrl = 'public/app/plugins/datasource/cloudwatch/partials/query.editor.html';
aliasSyntax: string;
/** @ngInject **/
constructor($scope, $injector) {
super($scope, $injector);
this.aliasSyntax = '{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}';
}
}

View File

@ -9,7 +9,7 @@ function (angular, _) {
module.directive('cloudwatchQueryParameter', function() {
return {
templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.parameter.html',
templateUrl: 'public/app/plugins/datasource/cloudwatch/partials/query.parameter.html',
controller: 'CloudWatchQueryParameterCtrl',
restrict: 'E',
scope: {
@ -102,7 +102,7 @@ function (angular, _) {
var query = $q.when([]);
if (segment.type === 'key' || segment.type === 'plus-button') {
query = $scope.datasource.getDimensionKeys($scope.target.namespace);
query = $scope.datasource.getDimensionKeys($scope.target.namespace, $scope.target.region);
} else if (segment.type === 'value') {
var dimensionKey = $scope.dimSegments[$index-2].value;
query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {});
@ -160,7 +160,7 @@ function (angular, _) {
};
$scope.getMetrics = function() {
return $scope.datasource.metricFindQuery('metrics(' + $scope.target.namespace + ')')
return $scope.datasource.metricFindQuery('metrics(' + $scope.target.namespace + ',' + $scope.target.region + ')')
.then($scope.transformToSegments(true));
};

View File

@ -3,7 +3,7 @@ import "../datasource";
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import Datasource from "../datasource";
import {CloudWatchDatasource} from "../datasource";
describe('CloudWatchDatasource', function() {
var ctx = new helpers.ServiceTestContext();
@ -20,7 +20,7 @@ describe('CloudWatchDatasource', function() {
ctx.$q = $q;
ctx.$httpBackend = $httpBackend;
ctx.$rootScope = $rootScope;
ctx.ds = $injector.instantiate(Datasource, {instanceSettings: instanceSettings});
ctx.ds = $injector.instantiate(CloudWatchDatasource, {instanceSettings: instanceSettings});
}));
describe('When performing CloudWatch query', function() {

View File

@ -0,0 +1,34 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class ElasticConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/config.html';
current: any;
/** @ngInject */
constructor($scope) {
this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp';
}
indexPatternTypes = [
{name: 'No pattern', value: undefined},
{name: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH'},
{name: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD'},
{name: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW'},
{name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM'},
{name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'},
];
esVersions = [
{name: '1.x', value: 1},
{name: '2.x', value: 2},
];
indexPatternTypeChanged() {
var def = _.findWhere(this.indexPatternTypes, {value: this.current.jsonData.interval});
this.current.database = def.example || 'es-index-name';
}
}

View File

@ -1,3 +1,3 @@
declare var Datasource: any;
export default Datasource;
declare var ElasticDatasource: any;
export {ElasticDatasource};

View File

@ -304,5 +304,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
};
}
return ElasticDatasource;
return {
ElasticDatasource: ElasticDatasource
};
});

View File

@ -1,39 +0,0 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class EditViewCtrl {
/** @ngInject */
constructor($scope) {
$scope.indexPatternTypes = [
{name: 'No pattern', value: undefined},
{name: 'Hourly', value: 'Hourly', example: '[logstash-]YYYY.MM.DD.HH'},
{name: 'Daily', value: 'Daily', example: '[logstash-]YYYY.MM.DD'},
{name: 'Weekly', value: 'Weekly', example: '[logstash-]GGGG.WW'},
{name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM'},
{name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY'},
];
$scope.esVersions = [
{name: '1.x', value: 1},
{name: '2.x', value: 2},
];
$scope.indexPatternTypeChanged = function() {
var def = _.findWhere($scope.indexPatternTypes, {value: $scope.current.jsonData.interval});
$scope.current.database = def.example || 'es-index-name';
};
}
}
function editViewDirective() {
return {
templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/edit_view.html',
controller: EditViewCtrl,
};
};
export default editViewDirective;

View File

@ -1,30 +0,0 @@
define([
'./datasource',
'./edit_view',
'./bucket_agg',
'./metric_agg',
],
function (ElasticDatasource, editView) {
'use strict';
function metricsQueryEditor() {
return {controller: 'ElasticQueryCtrl', templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/query.editor.html'};
}
function metricsQueryOptions() {
return {templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/query.options.html'};
}
function annotationsQueryEditor() {
return {templateUrl: 'public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html'};
}
return {
Datasource: ElasticDatasource,
configView: editView.default,
annotationsQueryEditor: annotationsQueryEditor,
metricsQueryEditor: metricsQueryEditor,
metricsQueryOptions: metricsQueryOptions,
};
});

View File

@ -0,0 +1,19 @@
import {ElasticDatasource} from './datasource';
import {ElasticQueryCtrl} from './query_ctrl';
import {ElasticConfigCtrl} from './config_ctrl';
class ElasticQueryOptionsCtrl {
static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/query.options.html';
}
class ElasticAnnotationsQueryCtrl {
static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/annotations.editor.html';
}
export {
ElasticDatasource as Datasource,
ElasticQueryCtrl as QueryCtrl,
ElasticConfigCtrl as ConfigCtrl,
ElasticQueryOptionsCtrl as QueryOptionsCtrl,
ElasticAnnotationsQueryCtrl as AnnotationsQueryCtrl,
};

View File

@ -1,14 +1,14 @@
<div class="editor-row">
<div class="section" ng-if="annotation.index">
<div class="section" ng-if="ctrl.annotation.index">
<h5>Index name</h5>
<div class="editor-option">
<input type="text" class="span4" ng-model='annotation.index' placeholder="events-*"></input>
<input type="text" class="span4" ng-model='ctrl.annotation.index' placeholder="events-*"></input>
</div>
</div>
<div class="section">
<h5>Search query (lucene) <tip>Use [[filterName]] in query to replace part of the query with a filter value</tip></h5>
<div class="editor-option">
<input type="text" class="span6" ng-model='annotation.query' placeholder="tags:deploy"></input>
<input type="text" class="span6" ng-model='ctrl.annotation.query' placeholder="tags:deploy"></input>
</div>
</div>
</div>
@ -18,22 +18,22 @@
<h5>Field mappings</h5>
<div class="editor-option">
<label class="small">Time</label>
<input type="text" class="input-small" ng-model='annotation.timeField' placeholder="@timestamp"></input>
<input type="text" class="input-small" ng-model='ctrl.annotation.timeField' placeholder="@timestamp"></input>
</div>
<div class="editor-option">
<label class="small">Title</label>
<input type="text" class="input-small" ng-model='annotation.titleField' placeholder="desc"></input>
<input type="text" class="input-small" ng-model='ctrl.annotation.titleField' placeholder="desc"></input>
</div>
<div class="editor-option">
<label class="small">Tags</label>
<input type="text" class="input-small" ng-model='annotation.tagsField' placeholder="tags"></input>
<input type="text" class="input-small" ng-model='ctrl.annotation.tagsField' placeholder="tags"></input>
</div>
<div class="editor-option">
<label class="small">Text</label>
<input type="text" class="input-small" ng-model='annotation.textField' placeholder=""></input>
<input type="text" class="input-small" ng-model='ctrl.annotation.textField' placeholder=""></input>
</div>
</div>
</div>

View File

@ -1,4 +1,5 @@
<datasource-http-settings></datasource-http-settings>
<datasource-http-settings current="ctrl.current">
</datasource-http-settings>
<h4>Elasticsearch details</h4>
@ -8,13 +9,13 @@
Index name
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.database' placeholder="" required></input>
<input type="text" class="tight-form-input input-xlarge" ng-model='ctrl.current.database' placeholder="" required></input>
</li>
<li class="tight-form-item">
Pattern
</li>
<li>
<select class="input-medium tight-form-input" ng-model="current.jsonData.interval" ng-options="f.value as f.name for f in indexPatternTypes" ng-change="indexPatternTypeChanged()" ></select>
<select class="input-medium tight-form-input" ng-model="ctrl.current.jsonData.interval" ng-options="f.value as f.name for f in ctrl.indexPatternTypes" ng-change="ctrl.indexPatternTypeChanged()" ></select>
</li>
</ul>
<div class="clearfix"></div>
@ -25,7 +26,7 @@
Time field name
</li>
<li>
<input type="text" class="tight-form-input input-xlarge" ng-model='current.jsonData.timeField' placeholder="" required ng-init="current.jsonData.timeField = current.jsonData.timeField || '@timestamp'"></input>
<input type="text" class="tight-form-input input-xlarge" ng-model='ctrl.current.jsonData.timeField' placeholder="" required ng-init=""></input>
</li>
</ul>
<div class="clearfix"></div>
@ -36,7 +37,7 @@
Version
</li>
<li>
<select class="input-medium tight-form-input" ng-model="current.jsonData.esVersion" ng-options="f.value as f.name for f in esVersions"></select>
<select class="input-medium tight-form-input" ng-model="ctrl.current.jsonData.esVersion" ng-options="f.value as f.name for f in ctrl.esVersions"></select>
</li>
</ul>
<div class="clearfix"></div>
@ -52,7 +53,7 @@
Group by time interval
</li>
<li>
<input type="text" class="input-medium tight-form-input input-xlarge" ng-model="current.jsonData.timeInterval"
<input type="text" class="input-medium tight-form-input input-xlarge" ng-model="ctrl.current.jsonData.timeInterval"
spellcheck='false' placeholder="example: >10s">
</li>
<li class="tight-form-item">

View File

@ -1,77 +1,32 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="target.datasource">
<em>{{target.datasource}}</em>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.duplicateDataQuery(target)">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="panelCtrl.moveDataQuery($index, $index+1)">Move down</a></li>
</ul>
</div>
</li>
<query-editor-row ctrl="ctrl">
<li class="tight-form-item query-keyword" style="width: 75px">
Query
</li>
<li>
<input type="text" class="tight-form-input" style="width: 345px;" ng-model="ctrl.target.query" spellcheck='false' placeholder="Lucene query" ng-blur="ctrl.refresh()">
</li>
<li class="tight-form-item query-keyword">
Alias
</li>
<li>
<input type="text" class="tight-form-input" style="width: 200px;" ng-model="ctrl.target.alias" spellcheck='false' placeholder="alias patterns (empty = auto)" ng-blur="ctrl.refresh()">
</li>
</query-editor-row>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="panelCtrl.removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="target.hide = !target.hide; panelCtrl.refresh();" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item query-keyword" style="width: 75px">
Query
</li>
<li>
<input type="text" class="tight-form-input" style="width: 345px;" ng-model="target.query" spellcheck='false' placeholder="Lucene query" ng-blur="panelCtrl.refresh()">
</li>
<li class="tight-form-item query-keyword">
Alias
</li>
<li>
<input type="text" class="tight-form-input" style="width: 200px;" ng-model="target.alias" spellcheck='false' placeholder="alias patterns (empty = auto)" ng-blur="panelCtrl.refresh()">
</li>
</ul>
<div class="clearfix"></div>
<div ng-repeat="agg in ctrl.target.metrics">
<elastic-metric-agg
target="ctrl.target" index="$index"
get-fields="ctrl.getFields($fieldType)"
on-change="ctrl.queryUpdated()"
es-version="ctrl.esVersion">
</elastic-metric-agg>
</div>
<div ng-hide="target.rawQuery">
<div ng-repeat="agg in target.metrics">
<elastic-metric-agg
target="target" index="$index"
get-fields="getFields($fieldType)"
on-change="queryUpdated()"
es-version="esVersion">
</elastic-metric-agg>
</div>
<div ng-repeat="agg in target.bucketAggs">
<elastic-bucket-agg
target="target" index="$index"
get-fields="getFields($fieldType)"
on-change="queryUpdated()">
</elastic-bucket-agg>
</div>
<div ng-repeat="agg in ctrl.target.bucketAggs">
<elastic-bucket-agg
target="ctrl.target" index="$index"
get-fields="ctrl.getFields($fieldType)"
on-change="ctrl.queryUpdated()">
</elastic-bucket-agg>
</div>

View File

@ -8,7 +8,7 @@
Group by time interval
</li>
<li>
<input type="text" class="input-medium tight-form-input" ng-model="ctrl.panel.interval" ng-blur="ctrl.refresh();"
<input type="text" class="input-medium tight-form-input" ng-model="ctrl.panelCtrl.panel.interval" ng-blur="ctrl.panelCtrl.refresh();"
spellcheck='false' placeholder="example: >10s">
</li>
<li class="tight-form-item">
@ -23,7 +23,7 @@
<i class="fa fa-info-circle"></i>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
alias patterns
</a>
</li>
@ -34,7 +34,7 @@
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 1">
<div class="grafana-info-box span6" 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>

View File

@ -1,46 +0,0 @@
define([
'angular',
],
function (angular) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('ElasticQueryCtrl', function($scope, $rootScope, $timeout, uiSegmentSrv) {
$scope.esVersion = $scope.datasource.esVersion;
$scope.panelCtrl = $scope.ctrl;
$scope.init = function() {
var target = $scope.target;
if (!target) { return; }
$scope.queryUpdated();
};
$scope.getFields = function(type) {
var jsonStr = angular.toJson({find: 'fields', type: type});
return $scope.datasource.metricFindQuery(jsonStr)
.then(uiSegmentSrv.transformToSegments(false))
.then(null, $scope.handleQueryError);
};
$scope.queryUpdated = function() {
var newJson = angular.toJson($scope.datasource.queryBuilder.build($scope.target), true);
if (newJson !== $scope.oldQueryRaw) {
$scope.rawQueryOld = newJson;
$scope.panelCtrl.refresh();
}
$rootScope.appEvent('elastic-query-updated');
};
$scope.handleQueryError = function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
};
$scope.init();
});
});

View File

@ -0,0 +1,45 @@
///<reference path="../../../headers/common.d.ts" />
import './bucket_agg';
import './metric_agg';
import angular from 'angular';
import _ from 'lodash';
import {QueryCtrl} from 'app/features/panel/panel';
export class ElasticQueryCtrl extends QueryCtrl {
static templateUrl = 'public/app/plugins/datasource/elasticsearch/partials/query.editor.html';
esVersion: any;
rawQueryOld: string;
/** @ngInject **/
constructor($scope, $injector, private $rootScope, private $timeout, private uiSegmentSrv) {
super($scope, $injector);
this.esVersion = this.datasource.esVersion;
this.queryUpdated();
}
getFields(type) {
var jsonStr = angular.toJson({find: 'fields', type: type});
return this.datasource.metricFindQuery(jsonStr)
.then(this.uiSegmentSrv.transformToSegments(false))
.catch(this.handleQueryError.bind(this));
}
queryUpdated() {
var newJson = angular.toJson(this.datasource.queryBuilder.build(this.target), true);
if (newJson !== this.rawQueryOld) {
this.rawQueryOld = newJson;
this.refresh();
}
this.$rootScope.appEvent('elastic-query-updated');
}
handleQueryError(err) {
this.error = err.message || 'Failed to issue metric query';
return [];
}
}

View File

@ -3,7 +3,7 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/co
import moment from 'moment';
import angular from 'angular';
import helpers from 'test/specs/helpers';
import Datasource from "../datasource";
import {ElasticDatasource} from "../datasource";
describe('ElasticDatasource', function() {
var ctx = new helpers.ServiceTestContext();
@ -21,7 +21,7 @@ describe('ElasticDatasource', function() {
function createDatasource(instanceSettings) {
instanceSettings.jsonData = instanceSettings.jsonData || {};
ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings});
ctx.ds = ctx.$injector.instantiate(ElasticDatasource, {instanceSettings: instanceSettings});
}
describe('When testing datasource with index pattern', function() {

View File

@ -1,29 +0,0 @@
///<amd-dependency path="../query_ctrl" />
///<amd-dependency path="app/core/services/segment_srv" />
///<amd-dependency path="test/specs/helpers" name="helpers" />
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import helpers from 'test/specs/helpers';
describe('ElasticQueryCtrl', function() {
var ctx = new helpers.ControllerTestContext();
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(ctx.providePhase());
beforeEach(ctx.createControllerPhase('ElasticQueryCtrl'));
beforeEach(function() {
ctx.scope.target = {};
ctx.scope.$parent = { get_data: sinon.spy() };
ctx.scope.datasource = ctx.datasource;
ctx.scope.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
});
describe('init', function() {
beforeEach(function() {
ctx.scope.init();
});
});
});

View File

@ -2,17 +2,15 @@
import angular from 'angular';
import {GrafanaDatasource} from './datasource';
import {QueryCtrl} from 'app/features/panel/panel';
var module = angular.module('grafana.directives');
function grafanaMetricsQueryEditor() {
return {templateUrl: 'public/app/plugins/datasource/grafana/partials/query.editor.html'};
class GrafanaQueryCtrl extends QueryCtrl {
static templateUrl = 'public/app/plugins/datasource/grafana/partials/query.editor.html';
}
export {
GrafanaDatasource,
GrafanaDatasource as Datasource,
grafanaMetricsQueryEditor as metricsQueryEditor
GrafanaQueryCtrl as QueryCtrl,
};

View File

@ -1,56 +1,5 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1"
ng-click="duplicate()">
Duplicate
</a>
</li>
<li role="menuitem">
<a tabindex="1"
ng-click="moveMetricQuery($index, $index-1)">
Move up
</a>
</li>
<li role="menuitem">
<a tabindex="1"
ng-click="moveMetricQuery($index, $index+1)">
Move down
</a>
</li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
<li class="tight-form-item">
Test metric (fake data source)
</li>
</ul>
<div class="clearfix"></div>
</div>
<query-editor-row ctrl="ctrl">
<li class="tight-form-item">
Test metric (fake data source)
</li>
</query-editor-row>

View File

@ -22,6 +22,7 @@ function (angular, _, $, gfunc) {
link: function($scope, elem) {
var categories = gfunc.getCategories();
var allFunctions = getAllFunctionNames(categories);
var ctrl = $scope.ctrl;
$scope.functionMenu = createFunctionDropDownMenu(categories);
@ -48,7 +49,7 @@ function (angular, _, $, gfunc) {
}
$scope.$apply(function() {
$scope.addFunction(funcDef);
ctrl.addFunction(funcDef);
});
$input.trigger('blur');

View File

@ -1,3 +0,0 @@
declare var Datasource: any;
export default Datasource;

View File

@ -1,296 +0,0 @@
define([
'angular',
'lodash',
'jquery',
'app/core/config',
'app/core/utils/datemath',
'./query_ctrl',
'./func_editor',
'./add_graphite_func',
],
function (angular, _, $, config, dateMath) {
'use strict';
/** @ngInject */
function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
this.basicAuth = instanceSettings.basicAuth;
this.url = instanceSettings.url;
this.name = instanceSettings.name;
this.cacheTimeout = instanceSettings.cacheTimeout;
this.withCredentials = instanceSettings.withCredentials;
this.render_method = instanceSettings.render_method || 'POST';
this.query = function(options) {
try {
var graphOptions = {
from: this.translateTime(options.rangeRaw.from, false),
until: this.translateTime(options.rangeRaw.to, true),
targets: options.targets,
format: options.format,
cacheTimeout: options.cacheTimeout || this.cacheTimeout,
maxDataPoints: options.maxDataPoints,
};
var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
if (params.length === 0) {
return $q.when([]);
}
if (options.format === 'png') {
return $q.when(this.url + '/render' + '?' + params.join('&'));
}
var httpOptions = { method: this.render_method, url: '/render' };
if (httpOptions.method === 'GET') {
httpOptions.url = httpOptions.url + '?' + params.join('&');
}
else {
httpOptions.data = params.join('&');
httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
}
return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
}
catch(err) {
return $q.reject(err);
}
};
this.convertDataPointsToMs = function(result) {
if (!result || !result.data) { return []; }
for (var i = 0; i < result.data.length; i++) {
var series = result.data[i];
for (var y = 0; y < series.datapoints.length; y++) {
series.datapoints[y][1] *= 1000;
}
}
return result;
};
this.annotationQuery = function(options) {
// Graphite metric as annotation
if (options.annotation.target) {
var target = templateSrv.replace(options.annotation.target);
var graphiteQuery = {
rangeRaw: options.rangeRaw,
targets: [{ target: target }],
format: 'json',
maxDataPoints: 100
};
return this.query(graphiteQuery)
.then(function(result) {
var list = [];
for (var i = 0; i < result.data.length; i++) {
var target = result.data[i];
for (var y = 0; y < target.datapoints.length; y++) {
var datapoint = target.datapoints[y];
if (!datapoint[0]) { continue; }
list.push({
annotation: options.annotation,
time: datapoint[1],
title: target.target
});
}
}
return list;
});
}
// Graphite event as annotation
else {
var tags = templateSrv.replace(options.annotation.tags);
return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
var list = [];
for (var i = 0; i < results.data.length; i++) {
var e = results.data[i];
list.push({
annotation: options.annotation,
time: e.when * 1000,
title: e.what,
tags: e.tags,
text: e.data
});
}
return list;
});
}
};
this.events = function(options) {
try {
var tags = '';
if (options.tags) {
tags = '&tags=' + options.tags;
}
return this.doGraphiteRequest({
method: 'GET',
url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
'&until=' + this.translateTime(options.range.to, true) + tags,
});
}
catch(err) {
return $q.reject(err);
}
};
this.translateTime = function(date, roundUp) {
if (_.isString(date)) {
if (date === 'now') {
return 'now';
}
else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
date = date.substring(3);
date = date.replace('m', 'min');
date = date.replace('M', 'mon');
return date;
}
date = dateMath.parse(date, roundUp);
}
// graphite' s from filter is exclusive
// here we step back one minute in order
// to guarantee that we get all the data that
// exists for the specified range
if (roundUp) {
if (date.get('s')) {
date.add(1, 'm');
}
}
else if (roundUp === false) {
if (date.get('s')) {
date.subtract(1, 'm');
}
}
return date.unix();
};
this.metricFindQuery = function(query) {
var interpolated;
try {
interpolated = encodeURIComponent(templateSrv.replace(query));
}
catch(err) {
return $q.reject(err);
}
return this.doGraphiteRequest({method: 'GET', url: '/metrics/find/?query=' + interpolated })
.then(function(results) {
return _.map(results.data, function(metric) {
return {
text: metric.text,
expandable: metric.expandable ? true : false
};
});
});
};
this.testDatasource = function() {
return this.metricFindQuery('*').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
});
};
this.listDashboards = function(query) {
return this.doGraphiteRequest({ method: 'GET', url: '/dashboard/find/', params: {query: query || ''} })
.then(function(results) {
return results.data.dashboards;
});
};
this.loadDashboard = function(dashName) {
return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
};
this.doGraphiteRequest = function(options) {
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
}
if (this.basicAuth) {
options.headers = options.headers || {};
options.headers.Authorization = this.basicAuth;
}
options.url = this.url + options.url;
options.inspect = { type: 'graphite' };
return backendSrv.datasourceRequest(options);
};
this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
this.buildGraphiteParams = function(options, scopedVars) {
var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
var clean_options = [], targets = {};
var target, targetValue, i;
var regex = /\#([A-Z])/g;
var intervalFormatFixRegex = /'(\d+)m'/gi;
var hasTargets = false;
if (options.format !== 'png') {
options['format'] = 'json';
}
function fixIntervalFormat(match) {
return match.replace('m', 'min').replace('M', 'mon');
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (!target.target) {
continue;
}
if (!target.refId) {
target.refId = this._seriesRefLetters[i];
}
targetValue = templateSrv.replace(target.target, scopedVars);
targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
targets[target.refId] = targetValue;
}
function nestedSeriesRegexReplacer(match, g1) {
return targets[g1];
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (!target.target) {
continue;
}
targetValue = targets[target.refId];
targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
targets[target.refId] = targetValue;
if (!target.hide) {
hasTargets = true;
clean_options.push("target=" + encodeURIComponent(targetValue));
}
}
_.each(options, function (value, key) {
if ($.inArray(key, graphite_options) === -1) { return; }
if (value) {
clean_options.push(key + "=" + encodeURIComponent(value));
}
});
if (!hasTargets) {
return [];
}
return clean_options;
};
}
return GraphiteDatasource;
});

View File

@ -0,0 +1,281 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
/** @ngInject */
export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
this.basicAuth = instanceSettings.basicAuth;
this.url = instanceSettings.url;
this.name = instanceSettings.name;
this.cacheTimeout = instanceSettings.cacheTimeout;
this.withCredentials = instanceSettings.withCredentials;
this.render_method = instanceSettings.render_method || 'POST';
this.query = function(options) {
try {
var graphOptions = {
from: this.translateTime(options.rangeRaw.from, false),
until: this.translateTime(options.rangeRaw.to, true),
targets: options.targets,
format: options.format,
cacheTimeout: options.cacheTimeout || this.cacheTimeout,
maxDataPoints: options.maxDataPoints,
};
var params = this.buildGraphiteParams(graphOptions, options.scopedVars);
if (params.length === 0) {
return $q.when([]);
}
if (options.format === 'png') {
return $q.when(this.url + '/render' + '?' + params.join('&'));
}
var httpOptions: any = {method: this.render_method, url: '/render'};
if (httpOptions.method === 'GET') {
httpOptions.url = httpOptions.url + '?' + params.join('&');
} else {
httpOptions.data = params.join('&');
httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
}
return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
} catch (err) {
return $q.reject(err);
}
};
this.convertDataPointsToMs = function(result) {
if (!result || !result.data) { return []; }
for (var i = 0; i < result.data.length; i++) {
var series = result.data[i];
for (var y = 0; y < series.datapoints.length; y++) {
series.datapoints[y][1] *= 1000;
}
}
return result;
};
this.annotationQuery = function(options) {
// Graphite metric as annotation
if (options.annotation.target) {
var target = templateSrv.replace(options.annotation.target);
var graphiteQuery = {
rangeRaw: options.rangeRaw,
targets: [{ target: target }],
format: 'json',
maxDataPoints: 100
};
return this.query(graphiteQuery)
.then(function(result) {
var list = [];
for (var i = 0; i < result.data.length; i++) {
var target = result.data[i];
for (var y = 0; y < target.datapoints.length; y++) {
var datapoint = target.datapoints[y];
if (!datapoint[0]) { continue; }
list.push({
annotation: options.annotation,
time: datapoint[1],
title: target.target
});
}
}
return list;
});
} else {
// Graphite event as annotation
var tags = templateSrv.replace(options.annotation.tags);
return this.events({range: options.rangeRaw, tags: tags}).then(function(results) {
var list = [];
for (var i = 0; i < results.data.length; i++) {
var e = results.data[i];
list.push({
annotation: options.annotation,
time: e.when * 1000,
title: e.what,
tags: e.tags,
text: e.data
});
}
return list;
});
}
};
this.events = function(options) {
try {
var tags = '';
if (options.tags) {
tags = '&tags=' + options.tags;
}
return this.doGraphiteRequest({
method: 'GET',
url: '/events/get_data?from=' + this.translateTime(options.range.from, false) +
'&until=' + this.translateTime(options.range.to, true) + tags,
});
} catch (err) {
return $q.reject(err);
}
};
this.translateTime = function(date, roundUp) {
if (_.isString(date)) {
if (date === 'now') {
return 'now';
} else if (date.indexOf('now-') >= 0 && date.indexOf('/') === -1) {
date = date.substring(3);
date = date.replace('m', 'min');
date = date.replace('M', 'mon');
return date;
}
date = dateMath.parse(date, roundUp);
}
// graphite' s from filter is exclusive
// here we step back one minute in order
// to guarantee that we get all the data that
// exists for the specified range
if (roundUp) {
if (date.get('s')) {
date.add(1, 'm');
}
} else if (roundUp === false) {
if (date.get('s')) {
date.subtract(1, 'm');
}
}
return date.unix();
};
this.metricFindQuery = function(query) {
var interpolated;
try {
interpolated = encodeURIComponent(templateSrv.replace(query));
} catch (err) {
return $q.reject(err);
}
return this.doGraphiteRequest({method: 'GET', url: '/metrics/find/?query=' + interpolated })
.then(function(results) {
return _.map(results.data, function(metric) {
return {
text: metric.text,
expandable: metric.expandable ? true : false
};
});
});
};
this.testDatasource = function() {
return this.metricFindQuery('*').then(function () {
return { status: "success", message: "Data source is working", title: "Success" };
});
};
this.listDashboards = function(query) {
return this.doGraphiteRequest({ method: 'GET', url: '/dashboard/find/', params: {query: query || ''} })
.then(function(results) {
return results.data.dashboards;
});
};
this.loadDashboard = function(dashName) {
return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
};
this.doGraphiteRequest = function(options) {
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
}
if (this.basicAuth) {
options.headers = options.headers || {};
options.headers.Authorization = this.basicAuth;
}
options.url = this.url + options.url;
options.inspect = { type: 'graphite' };
return backendSrv.datasourceRequest(options);
};
this._seriesRefLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
this.buildGraphiteParams = function(options, scopedVars) {
var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout'];
var clean_options = [], targets = {};
var target, targetValue, i;
var regex = /\#([A-Z])/g;
var intervalFormatFixRegex = /'(\d+)m'/gi;
var hasTargets = false;
if (options.format !== 'png') {
options['format'] = 'json';
}
function fixIntervalFormat(match) {
return match.replace('m', 'min').replace('M', 'mon');
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (!target.target) {
continue;
}
if (!target.refId) {
target.refId = this._seriesRefLetters[i];
}
targetValue = templateSrv.replace(target.target, scopedVars);
targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat);
targets[target.refId] = targetValue;
}
function nestedSeriesRegexReplacer(match, g1) {
return targets[g1];
}
for (i = 0; i < options.targets.length; i++) {
target = options.targets[i];
if (!target.target) {
continue;
}
targetValue = targets[target.refId];
targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer);
targets[target.refId] = targetValue;
if (!target.hide) {
hasTargets = true;
clean_options.push("target=" + encodeURIComponent(targetValue));
}
}
_.each(options, function (value, key) {
if (_.indexOf(graphite_options, key) === -1) { return; }
if (value) {
clean_options.push(key + "=" + encodeURIComponent(value));
}
});
if (!hasTargets) {
return [];
}
return clean_options;
};
}

View File

@ -27,6 +27,7 @@ function (angular, _, $) {
link: function postLink($scope, elem) {
var $funcLink = $(funcSpanTemplate);
var $funcControls = $(funcControlsTemplate);
var ctrl = $scope.ctrl;
var func = $scope.func;
var funcDef = func.def;
var scheduledRelink = false;
@ -79,11 +80,13 @@ function (angular, _, $) {
func.updateParam($input.val(), paramIndex);
scheduledRelinkIfNeeded();
$scope.$apply($scope.targetChanged);
}
$scope.$apply(function() {
ctrl.targetChanged();
});
$input.hide();
$link.show();
$input.hide();
$link.show();
}
}
function inputKeyPress(paramIndex, e) {
@ -198,7 +201,7 @@ function (angular, _, $) {
if ($target.hasClass('fa-remove')) {
toggleFuncControls();
$scope.$apply(function() {
$scope.removeFunction($scope.func);
ctrl.removeFunction($scope.func);
});
return;
}
@ -206,7 +209,7 @@ function (angular, _, $) {
if ($target.hasClass('fa-arrow-left')) {
$scope.$apply(function() {
_.move($scope.functions, $scope.$index, $scope.$index - 1);
$scope.targetChanged();
ctrl.targetChanged();
});
return;
}
@ -214,7 +217,7 @@ function (angular, _, $) {
if ($target.hasClass('fa-arrow-right')) {
$scope.$apply(function() {
_.move($scope.functions, $scope.$index, $scope.$index + 1);
$scope.targetChanged();
ctrl.targetChanged();
});
return;
}

View File

@ -1,682 +0,0 @@
define([
'lodash'
], function(_) {
'use strict';
// This is auto generated from the unicode tables.
// The tables are at:
// http://www.fileformat.info/info/unicode/category/Lu/list.htm
// http://www.fileformat.info/info/unicode/category/Ll/list.htm
// http://www.fileformat.info/info/unicode/category/Lt/list.htm
// http://www.fileformat.info/info/unicode/category/Lm/list.htm
// http://www.fileformat.info/info/unicode/category/Lo/list.htm
// http://www.fileformat.info/info/unicode/category/Nl/list.htm
var unicodeLetterTable = [
170, 170, 181, 181, 186, 186, 192, 214,
216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750,
880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908,
910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366,
1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610,
1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775,
1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957,
1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069,
2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361,
2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431,
2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482,
2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608,
2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654,
2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736,
2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785,
2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867,
2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970,
2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001,
3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123,
3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212,
3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261,
3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344,
3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455,
3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526,
3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716,
3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760,
3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805,
3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138,
4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198,
4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346,
4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696,
4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789,
4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000,
6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312,
6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516,
6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823,
6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141,
7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409,
7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013,
8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061,
8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140,
8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188,
8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455,
8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486,
8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358,
11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621,
11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694,
11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295,
12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438,
12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589,
12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312,
19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124,
42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539,
42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783,
42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921,
43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042,
43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259,
43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442,
43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595,
43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697,
43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714,
43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798,
43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032,
55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045,
64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279,
64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316,
64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370,
65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495,
65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594,
65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786,
65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334,
66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511,
66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592,
67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669,
67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115,
68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405,
68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687,
69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894,
92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964,
119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980,
119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069,
120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121,
120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144,
120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570,
120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686,
120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779,
131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972,
177984, 177984, 178205, 178205, 194560, 195101
];
var identifierStartTable = [];
for (var i = 0; i < 128; i++) {
identifierStartTable[i] =
i >= 48 && i <= 57 || // 0-9
i === 36 || // $
i === 126 || // ~
i === 124 || // |
i >= 65 && i <= 90 || // A-Z
i === 95 || // _
i === 45 || // -
i === 42 || // *
i === 58 || // :
i === 91 || // templateStart [
i === 93 || // templateEnd ]
i === 63 || // ?
i === 37 || // %
i === 35 || // #
i === 61 || // =
i >= 97 && i <= 122; // a-z
}
var identifierPartTable = [];
for (var i2 = 0; i2 < 128; i2++) {
identifierPartTable[i2] =
identifierStartTable[i2] || // $, _, A-Z, a-z
i2 >= 48 && i2 <= 57; // 0-9
}
function Lexer(expression) {
this.input = expression;
this.char = 1;
this.from = 1;
}
Lexer.prototype = {
peek: function (i) {
return this.input.charAt(i || 0);
},
skip: function (i) {
i = i || 1;
this.char += i;
this.input = this.input.slice(i);
},
tokenize: function() {
var list = [];
var token;
while (token = this.next()) {
list.push(token);
}
return list;
},
next: function() {
this.from = this.char;
// Move to the next non-space character.
var start;
if (/\s/.test(this.peek())) {
start = this.char;
while (/\s/.test(this.peek())) {
this.from += 1;
this.skip();
}
if (this.peek() === "") { // EOL
return null;
}
}
var match = this.scanStringLiteral();
if (match) {
return match;
}
match =
this.scanPunctuator() ||
this.scanNumericLiteral() ||
this.scanIdentifier() ||
this.scanTemplateSequence();
if (match) {
this.skip(match.value.length);
return match;
}
// No token could be matched, give up.
return null;
},
scanTemplateSequence: function() {
if (this.peek() === '[' && this.peek(1) === '[') {
return {
type: 'templateStart',
value: '[[',
pos: this.char
};
}
if (this.peek() === ']' && this.peek(1) === ']') {
return {
type: 'templateEnd',
value: '[[',
pos: this.char
};
}
return null;
},
/*
* Extract a JavaScript identifier out of the next sequence of
* characters or return 'null' if its not possible. In addition,
* to Identifier this method can also produce BooleanLiteral
* (true/false) and NullLiteral (null).
*/
scanIdentifier: function() {
var id = "";
var index = 0;
var type, char;
// Detects any character in the Unicode categories "Uppercase
// letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter
// (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or
// "Letter number (Nl)".
//
// Both approach and unicodeLetterTable were borrowed from
// Google's Traceur.
function isUnicodeLetter(code) {
for (var i = 0; i < unicodeLetterTable.length;) {
if (code < unicodeLetterTable[i++]) {
return false;
}
if (code <= unicodeLetterTable[i++]) {
return true;
}
}
return false;
}
function isHexDigit(str) {
return (/^[0-9a-fA-F]$/).test(str);
}
var readUnicodeEscapeSequence = _.bind(function () {
/*jshint validthis:true */
index += 1;
if (this.peek(index) !== "u") {
return null;
}
var ch1 = this.peek(index + 1);
var ch2 = this.peek(index + 2);
var ch3 = this.peek(index + 3);
var ch4 = this.peek(index + 4);
var code;
if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) {
code = parseInt(ch1 + ch2 + ch3 + ch4, 16);
if (isUnicodeLetter(code)) {
index += 5;
return "\\u" + ch1 + ch2 + ch3 + ch4;
}
return null;
}
return null;
}, this);
var getIdentifierStart = _.bind(function () {
/*jshint validthis:true */
var chr = this.peek(index);
var code = chr.charCodeAt(0);
if (chr === '*') {
index += 1;
return chr;
}
if (code === 92) {
return readUnicodeEscapeSequence();
}
if (code < 128) {
if (identifierStartTable[code]) {
index += 1;
return chr;
}
return null;
}
if (isUnicodeLetter(code)) {
index += 1;
return chr;
}
return null;
}, this);
var getIdentifierPart = _.bind(function () {
/*jshint validthis:true */
var chr = this.peek(index);
var code = chr.charCodeAt(0);
if (code === 92) {
return readUnicodeEscapeSequence();
}
if (code < 128) {
if (identifierPartTable[code]) {
index += 1;
return chr;
}
return null;
}
if (isUnicodeLetter(code)) {
index += 1;
return chr;
}
return null;
}, this);
char = getIdentifierStart();
if (char === null) {
return null;
}
id = char;
for (;;) {
char = getIdentifierPart();
if (char === null) {
break;
}
id += char;
}
switch (id) {
case 'true': {
type = 'bool';
break;
}
case 'false': {
type = 'bool';
break;
}
default:
type = "identifier";
}
return {
type: type,
value: id,
pos: this.char
};
},
/*
* Extract a numeric literal out of the next sequence of
* characters or return 'null' if its not possible. This method
* supports all numeric literals described in section 7.8.3
* of the EcmaScript 5 specification.
*
* This method's implementation was heavily influenced by the
* scanNumericLiteral function in the Esprima parser's source code.
*/
scanNumericLiteral: function () {
var index = 0;
var value = "";
var length = this.input.length;
var char = this.peek(index);
var bad;
function isDecimalDigit(str) {
return (/^[0-9]$/).test(str);
}
function isOctalDigit(str) {
return (/^[0-7]$/).test(str);
}
function isHexDigit(str) {
return (/^[0-9a-fA-F]$/).test(str);
}
function isIdentifierStart(ch) {
return (ch === "$") || (ch === "_") || (ch === "\\") ||
(ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
}
// handle negative num literals
if (char === '-') {
value += char;
index += 1;
char = this.peek(index);
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}
if (char !== ".") {
value += this.peek(index);
index += 1;
char = this.peek(index);
if (value === "0") {
// Base-16 numbers.
if (char === "x" || char === "X") {
index += 1;
value += char;
while (index < length) {
char = this.peek(index);
if (!isHexDigit(char)) {
break;
}
value += char;
index += 1;
}
if (value.length <= 2) { // 0x
return {
type: 'number',
value: value,
isMalformed: true,
pos: this.char
};
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 16,
isMalformed: false,
pos: this.char
};
}
// Base-8 numbers.
if (isOctalDigit(char)) {
index += 1;
value += char;
bad = false;
while (index < length) {
char = this.peek(index);
// Numbers like '019' (note the 9) are not valid octals
// but we still parse them and mark as malformed.
if (isDecimalDigit(char)) {
bad = true;
} else if (!isOctalDigit(char)) {
break;
}
value += char;
index += 1;
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 8,
isMalformed: false
};
}
// Decimal numbers that start with '0' such as '09' are illegal
// but we still parse them and return as malformed.
if (isDecimalDigit(char)) {
index += 1;
value += char;
}
}
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Decimal digits.
if (char === ".") {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Exponent part.
if (char === "e" || char === "E") {
value += char;
index += 1;
char = this.peek(index);
if (char === "+" || char === "-") {
value += this.peek(index);
index += 1;
}
char = this.peek(index);
if (isDecimalDigit(char)) {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
} else {
return null;
}
}
if (index < length) {
char = this.peek(index);
if (!this.isPunctuator(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 10,
pos: this.char,
isMalformed: !isFinite(value)
};
},
isPunctuator: function (ch1) {
switch (ch1) {
case ".":
case "(":
case ")":
case ",":
case "{":
case "}":
return true;
}
return false;
},
scanPunctuator: function () {
var ch1 = this.peek();
if (this.isPunctuator(ch1)) {
return {
type: ch1,
value: ch1,
pos: this.char
};
}
return null;
},
/*
* Extract a string out of the next sequence of characters and/or
* lines or return 'null' if its not possible. Since strings can
* span across multiple lines this method has to move the char
* pointer.
*
* This method recognizes pseudo-multiline JavaScript strings:
*
* var str = "hello\
* world";
*/
scanStringLiteral: function () {
/*jshint loopfunc:true */
var quote = this.peek();
// String must start with a quote.
if (quote !== "\"" && quote !== "'") {
return null;
}
var value = "";
this.skip();
while (this.peek() !== quote) {
if (this.peek() === "") { // End Of Line
return {
type: 'string',
value: value,
isUnclosed: true,
quote: quote,
pos: this.char
};
}
var char = this.peek();
var jump = 1; // A length of a jump, after we're done
// parsing this character.
value += char;
this.skip(jump);
}
this.skip();
return {
type: 'string',
value: value,
isUnclosed: false,
quote: quote,
pos: this.char
};
},
};
return Lexer;
});

View File

@ -0,0 +1,678 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
// This is auto generated from the unicode tables.
// The tables are at:
// http://www.fileformat.info/info/unicode/category/Lu/list.htm
// http://www.fileformat.info/info/unicode/category/Ll/list.htm
// http://www.fileformat.info/info/unicode/category/Lt/list.htm
// http://www.fileformat.info/info/unicode/category/Lm/list.htm
// http://www.fileformat.info/info/unicode/category/Lo/list.htm
// http://www.fileformat.info/info/unicode/category/Nl/list.htm
var unicodeLetterTable = [
170, 170, 181, 181, 186, 186, 192, 214,
216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750,
880, 884, 886, 887, 890, 893, 902, 902, 904, 906, 908, 908,
910, 929, 931, 1013, 1015, 1153, 1162, 1319, 1329, 1366,
1369, 1369, 1377, 1415, 1488, 1514, 1520, 1522, 1568, 1610,
1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775,
1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957,
1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069,
2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2308, 2361,
2365, 2365, 2384, 2384, 2392, 2401, 2417, 2423, 2425, 2431,
2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482,
2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529,
2544, 2545, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608,
2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654,
2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736,
2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785,
2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867,
2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929,
2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970,
2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001,
3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3123,
3125, 3129, 3133, 3133, 3160, 3161, 3168, 3169, 3205, 3212,
3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261,
3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344,
3346, 3386, 3389, 3389, 3406, 3406, 3424, 3425, 3450, 3455,
3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526,
3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716,
3719, 3720, 3722, 3722, 3725, 3725, 3732, 3735, 3737, 3743,
3745, 3747, 3749, 3749, 3751, 3751, 3754, 3755, 3757, 3760,
3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3805,
3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138,
4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198,
4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4304, 4346,
4348, 4348, 4352, 4680, 4682, 4685, 4688, 4694, 4696, 4696,
4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789,
4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880,
4882, 4885, 4888, 4954, 4992, 5007, 5024, 5108, 5121, 5740,
5743, 5759, 5761, 5786, 5792, 5866, 5870, 5872, 5888, 5900,
5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000,
6016, 6067, 6103, 6103, 6108, 6108, 6176, 6263, 6272, 6312,
6314, 6314, 6320, 6389, 6400, 6428, 6480, 6509, 6512, 6516,
6528, 6571, 6593, 6599, 6656, 6678, 6688, 6740, 6823, 6823,
6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7104, 7141,
7168, 7203, 7245, 7247, 7258, 7293, 7401, 7404, 7406, 7409,
7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013,
8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061,
8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140,
8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188,
8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455,
8458, 8467, 8469, 8469, 8473, 8477, 8484, 8484, 8486, 8486,
8488, 8488, 8490, 8493, 8495, 8505, 8508, 8511, 8517, 8521,
8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358,
11360, 11492, 11499, 11502, 11520, 11557, 11568, 11621,
11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694,
11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726,
11728, 11734, 11736, 11742, 11823, 11823, 12293, 12295,
12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438,
12445, 12447, 12449, 12538, 12540, 12543, 12549, 12589,
12593, 12686, 12704, 12730, 12784, 12799, 13312, 13312,
19893, 19893, 19968, 19968, 40907, 40907, 40960, 42124,
42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539,
42560, 42606, 42623, 42647, 42656, 42735, 42775, 42783,
42786, 42888, 42891, 42894, 42896, 42897, 42912, 42921,
43002, 43009, 43011, 43013, 43015, 43018, 43020, 43042,
43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259,
43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442,
43471, 43471, 43520, 43560, 43584, 43586, 43588, 43595,
43616, 43638, 43642, 43642, 43648, 43695, 43697, 43697,
43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714,
43739, 43741, 43777, 43782, 43785, 43790, 43793, 43798,
43808, 43814, 43816, 43822, 43968, 44002, 44032, 44032,
55203, 55203, 55216, 55238, 55243, 55291, 63744, 64045,
64048, 64109, 64112, 64217, 64256, 64262, 64275, 64279,
64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316,
64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433,
64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019,
65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370,
65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495,
65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594,
65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786,
65856, 65908, 66176, 66204, 66208, 66256, 66304, 66334,
66352, 66378, 66432, 66461, 66464, 66499, 66504, 66511,
66513, 66517, 66560, 66717, 67584, 67589, 67592, 67592,
67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669,
67840, 67861, 67872, 67897, 68096, 68096, 68112, 68115,
68117, 68119, 68121, 68147, 68192, 68220, 68352, 68405,
68416, 68437, 68448, 68466, 68608, 68680, 69635, 69687,
69763, 69807, 73728, 74606, 74752, 74850, 77824, 78894,
92160, 92728, 110592, 110593, 119808, 119892, 119894, 119964,
119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980,
119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069,
120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121,
120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144,
120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570,
120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686,
120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779,
131072, 131072, 173782, 173782, 173824, 173824, 177972, 177972,
177984, 177984, 178205, 178205, 194560, 195101
];
var identifierStartTable = [];
for (var i = 0; i < 128; i++) {
identifierStartTable[i] =
i >= 48 && i <= 57 || // 0-9
i === 36 || // $
i === 126 || // ~
i === 124 || // |
i >= 65 && i <= 90 || // A-Z
i === 95 || // _
i === 45 || // -
i === 42 || // *
i === 58 || // :
i === 91 || // templateStart [
i === 93 || // templateEnd ]
i === 63 || // ?
i === 37 || // %
i === 35 || // #
i === 61 || // =
i >= 97 && i <= 122; // a-z
}
var identifierPartTable = [];
for (var i2 = 0; i2 < 128; i2++) {
identifierPartTable[i2] =
identifierStartTable[i2] || // $, _, A-Z, a-z
i2 >= 48 && i2 <= 57; // 0-9
}
export function Lexer(expression) {
this.input = expression;
this.char = 1;
this.from = 1;
}
Lexer.prototype = {
peek: function (i) {
return this.input.charAt(i || 0);
},
skip: function (i) {
i = i || 1;
this.char += i;
this.input = this.input.slice(i);
},
tokenize: function() {
var list = [];
var token;
while (token = this.next()) {
list.push(token);
}
return list;
},
next: function() {
this.from = this.char;
// Move to the next non-space character.
var start;
if (/\s/.test(this.peek())) {
start = this.char;
while (/\s/.test(this.peek())) {
this.from += 1;
this.skip();
}
if (this.peek() === "") { // EOL
return null;
}
}
var match = this.scanStringLiteral();
if (match) {
return match;
}
match =
this.scanPunctuator() ||
this.scanNumericLiteral() ||
this.scanIdentifier() ||
this.scanTemplateSequence();
if (match) {
this.skip(match.value.length);
return match;
}
// No token could be matched, give up.
return null;
},
scanTemplateSequence: function() {
if (this.peek() === '[' && this.peek(1) === '[') {
return {
type: 'templateStart',
value: '[[',
pos: this.char
};
}
if (this.peek() === ']' && this.peek(1) === ']') {
return {
type: 'templateEnd',
value: '[[',
pos: this.char
};
}
return null;
},
/*
* Extract a JavaScript identifier out of the next sequence of
* characters or return 'null' if its not possible. In addition,
* to Identifier this method can also produce BooleanLiteral
* (true/false) and NullLiteral (null).
*/
scanIdentifier: function() {
var id = "";
var index = 0;
var type, char;
// Detects any character in the Unicode categories "Uppercase
// letter (Lu)", "Lowercase letter (Ll)", "Titlecase letter
// (Lt)", "Modifier letter (Lm)", "Other letter (Lo)", or
// "Letter number (Nl)".
//
// Both approach and unicodeLetterTable were borrowed from
// Google's Traceur.
function isUnicodeLetter(code) {
for (var i = 0; i < unicodeLetterTable.length;) {
if (code < unicodeLetterTable[i++]) {
return false;
}
if (code <= unicodeLetterTable[i++]) {
return true;
}
}
return false;
}
function isHexDigit(str) {
return (/^[0-9a-fA-F]$/).test(str);
}
var readUnicodeEscapeSequence = _.bind(function () {
/*jshint validthis:true */
index += 1;
if (this.peek(index) !== "u") {
return null;
}
var ch1 = this.peek(index + 1);
var ch2 = this.peek(index + 2);
var ch3 = this.peek(index + 3);
var ch4 = this.peek(index + 4);
var code;
if (isHexDigit(ch1) && isHexDigit(ch2) && isHexDigit(ch3) && isHexDigit(ch4)) {
code = parseInt(ch1 + ch2 + ch3 + ch4, 16);
if (isUnicodeLetter(code)) {
index += 5;
return "\\u" + ch1 + ch2 + ch3 + ch4;
}
return null;
}
return null;
}, this);
var getIdentifierStart = _.bind(function () {
/*jshint validthis:true */
var chr = this.peek(index);
var code = chr.charCodeAt(0);
if (chr === '*') {
index += 1;
return chr;
}
if (code === 92) {
return readUnicodeEscapeSequence();
}
if (code < 128) {
if (identifierStartTable[code]) {
index += 1;
return chr;
}
return null;
}
if (isUnicodeLetter(code)) {
index += 1;
return chr;
}
return null;
}, this);
var getIdentifierPart = _.bind(function () {
/*jshint validthis:true */
var chr = this.peek(index);
var code = chr.charCodeAt(0);
if (code === 92) {
return readUnicodeEscapeSequence();
}
if (code < 128) {
if (identifierPartTable[code]) {
index += 1;
return chr;
}
return null;
}
if (isUnicodeLetter(code)) {
index += 1;
return chr;
}
return null;
}, this);
char = getIdentifierStart();
if (char === null) {
return null;
}
id = char;
for (;;) {
char = getIdentifierPart();
if (char === null) {
break;
}
id += char;
}
switch (id) {
case 'true': {
type = 'bool';
break;
}
case 'false': {
type = 'bool';
break;
}
default:
type = "identifier";
}
return {
type: type,
value: id,
pos: this.char
};
},
/*
* Extract a numeric literal out of the next sequence of
* characters or return 'null' if its not possible. This method
* supports all numeric literals described in section 7.8.3
* of the EcmaScript 5 specification.
*
* This method's implementation was heavily influenced by the
* scanNumericLiteral function in the Esprima parser's source code.
*/
scanNumericLiteral: function (): any {
var index = 0;
var value = "";
var length = this.input.length;
var char = this.peek(index);
var bad;
function isDecimalDigit(str) {
return (/^[0-9]$/).test(str);
}
function isOctalDigit(str) {
return (/^[0-7]$/).test(str);
}
function isHexDigit(str) {
return (/^[0-9a-fA-F]$/).test(str);
}
function isIdentifierStart(ch) {
return (ch === "$") || (ch === "_") || (ch === "\\") ||
(ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
}
// handle negative num literals
if (char === '-') {
value += char;
index += 1;
char = this.peek(index);
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}
if (char !== ".") {
value += this.peek(index);
index += 1;
char = this.peek(index);
if (value === "0") {
// Base-16 numbers.
if (char === "x" || char === "X") {
index += 1;
value += char;
while (index < length) {
char = this.peek(index);
if (!isHexDigit(char)) {
break;
}
value += char;
index += 1;
}
if (value.length <= 2) { // 0x
return {
type: 'number',
value: value,
isMalformed: true,
pos: this.char
};
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 16,
isMalformed: false,
pos: this.char
};
}
// Base-8 numbers.
if (isOctalDigit(char)) {
index += 1;
value += char;
bad = false;
while (index < length) {
char = this.peek(index);
// Numbers like '019' (note the 9) are not valid octals
// but we still parse them and mark as malformed.
if (isDecimalDigit(char)) {
bad = true;
} else if (!isOctalDigit(char)) {
break;
}
value += char;
index += 1;
}
if (index < length) {
char = this.peek(index);
if (isIdentifierStart(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 8,
isMalformed: false
};
}
// Decimal numbers that start with '0' such as '09' are illegal
// but we still parse them and return as malformed.
if (isDecimalDigit(char)) {
index += 1;
value += char;
}
}
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Decimal digits.
if (char === ".") {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
}
// Exponent part.
if (char === "e" || char === "E") {
value += char;
index += 1;
char = this.peek(index);
if (char === "+" || char === "-") {
value += this.peek(index);
index += 1;
}
char = this.peek(index);
if (isDecimalDigit(char)) {
value += char;
index += 1;
while (index < length) {
char = this.peek(index);
if (!isDecimalDigit(char)) {
break;
}
value += char;
index += 1;
}
} else {
return null;
}
}
if (index < length) {
char = this.peek(index);
if (!this.isPunctuator(char)) {
return null;
}
}
return {
type: 'number',
value: value,
base: 10,
pos: this.char,
isMalformed: !isFinite(+value)
};
},
isPunctuator: function (ch1) {
switch (ch1) {
case ".":
case "(":
case ")":
case ",":
case "{":
case "}":
return true;
}
return false;
},
scanPunctuator: function () {
var ch1 = this.peek();
if (this.isPunctuator(ch1)) {
return {
type: ch1,
value: ch1,
pos: this.char
};
}
return null;
},
/*
* Extract a string out of the next sequence of characters and/or
* lines or return 'null' if its not possible. Since strings can
* span across multiple lines this method has to move the char
* pointer.
*
* This method recognizes pseudo-multiline JavaScript strings:
*
* var str = "hello\
* world";
*/
scanStringLiteral: function () {
/*jshint loopfunc:true */
var quote = this.peek();
// String must start with a quote.
if (quote !== "\"" && quote !== "'") {
return null;
}
var value = "";
this.skip();
while (this.peek() !== quote) {
if (this.peek() === "") { // End Of Line
return {
type: 'string',
value: value,
isUnclosed: true,
quote: quote,
pos: this.char
};
}
var char = this.peek();
var jump = 1; // A length of a jump, after we're done
// parsing this character.
value += char;
this.skip(jump);
}
this.skip();
return {
type: 'string',
value: value,
isUnclosed: false,
quote: quote,
pos: this.char
};
},
};

View File

@ -1,33 +0,0 @@
define([
'./datasource',
],
function (GraphiteDatasource) {
'use strict';
function metricsQueryEditor() {
return {
controller: 'GraphiteQueryCtrl',
templateUrl: 'public/app/plugins/datasource/graphite/partials/query.editor.html'
};
}
function metricsQueryOptions() {
return {templateUrl: 'public/app/plugins/datasource/graphite/partials/query.options.html'};
}
function annotationsQueryEditor() {
return {templateUrl: 'public/app/plugins/datasource/graphite/partials/annotations.editor.html'};
}
function configView() {
return {templateUrl: 'public/app/plugins/datasource/graphite/partials/config.html'};
}
return {
Datasource: GraphiteDatasource,
configView: configView,
annotationsQueryEditor: annotationsQueryEditor,
metricsQueryEditor: metricsQueryEditor,
metricsQueryOptions: metricsQueryOptions,
};
});

View File

@ -0,0 +1,23 @@
import {GraphiteDatasource} from './datasource';
import {GraphiteQueryCtrl} from './query_ctrl';
class GraphiteConfigCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/config.html';
}
class GraphiteQueryOptionsCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/query.options.html';
}
class AnnotationsQueryCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/annotations.editor.html';
}
export {
GraphiteDatasource as Datasource,
GraphiteQueryCtrl as QueryCtrl,
GraphiteConfigCtrl as ConfigCtrl,
GraphiteQueryOptionsCtrl as QueryOptionsCtrl,
AnnotationsQueryCtrl as AnnotationsQueryCtrl,
};

View File

@ -1,265 +0,0 @@
define([
'./lexer'
], function (Lexer) {
'use strict';
function Parser(expression) {
this.expression = expression;
this.lexer = new Lexer(expression);
this.tokens = this.lexer.tokenize();
this.index = 0;
}
Parser.prototype = {
getAst: function () {
return this.start();
},
start: function () {
try {
return this.functionCall() || this.metricExpression();
}
catch (e) {
return {
type: 'error',
message: e.message,
pos: e.pos
};
}
},
curlyBraceSegment: function() {
if (this.match('identifier', '{') || this.match('{')) {
var curlySegment = "";
while (!this.match('') && !this.match('}')) {
curlySegment += this.consumeToken().value;
}
if (!this.match('}')) {
this.errorMark("Expected closing '}'");
}
curlySegment += this.consumeToken().value;
// if curly segment is directly followed by identifier
// include it in the segment
if (this.match('identifier')) {
curlySegment += this.consumeToken().value;
}
return {
type: 'segment',
value: curlySegment
};
}
else {
return null;
}
},
metricSegment: function() {
var curly = this.curlyBraceSegment();
if (curly) {
return curly;
}
if (this.match('identifier') || this.match('number')) {
// hack to handle float numbers in metric segments
var parts = this.consumeToken().value.split('.');
if (parts.length === 2) {
this.tokens.splice(this.index, 0, { type: '.' });
this.tokens.splice(this.index + 1, 0, { type: 'number', value: parts[1] });
}
return {
type: 'segment',
value: parts[0]
};
}
if (!this.match('templateStart')) {
this.errorMark('Expected metric identifier');
}
this.consumeToken();
if (!this.match('identifier')) {
this.errorMark('Expected identifier after templateStart');
}
var node = {
type: 'template',
value: this.consumeToken().value
};
if (!this.match('templateEnd')) {
this.errorMark('Expected templateEnd');
}
this.consumeToken();
return node;
},
metricExpression: function() {
if (!this.match('templateStart') &&
!this.match('identifier') &&
!this.match('number') &&
!this.match('{')) {
return null;
}
var node = {
type: 'metric',
segments: []
};
node.segments.push(this.metricSegment());
while (this.match('.')) {
this.consumeToken();
var segment = this.metricSegment();
if (!segment) {
this.errorMark('Expected metric identifier');
}
node.segments.push(segment);
}
return node;
},
functionCall: function() {
if (!this.match('identifier', '(')) {
return null;
}
var node = {
type: 'function',
name: this.consumeToken().value,
};
// consume left parenthesis
this.consumeToken();
node.params = this.functionParameters();
if (!this.match(')')) {
this.errorMark('Expected closing parenthesis');
}
this.consumeToken();
return node;
},
boolExpression: function() {
if (!this.match('bool')) {
return null;
}
return {
type: 'bool',
value: this.consumeToken().value === 'true',
};
},
functionParameters: function () {
if (this.match(')') || this.match('')) {
return [];
}
var param =
this.functionCall() ||
this.numericLiteral() ||
this.seriesRefExpression() ||
this.boolExpression() ||
this.metricExpression() ||
this.stringLiteral();
if (!this.match(',')) {
return [param];
}
this.consumeToken();
return [param].concat(this.functionParameters());
},
seriesRefExpression: function() {
if (!this.match('identifier')) {
return null;
}
var value = this.tokens[this.index].value;
if (!value.match(/\#[A-Z]/)) {
return null;
}
var token = this.consumeToken();
return {
type: 'series-ref',
value: token.value
};
},
numericLiteral: function () {
if (!this.match('number')) {
return null;
}
return {
type: 'number',
value: parseFloat(this.consumeToken().value)
};
},
stringLiteral: function () {
if (!this.match('string')) {
return null;
}
var token = this.consumeToken();
if (token.isUnclosed) {
throw { message: 'Unclosed string parameter', pos: token.pos };
}
return {
type: 'string',
value: token.value
};
},
errorMark: function(text) {
var currentToken = this.tokens[this.index];
var type = currentToken ? currentToken.type : 'end of string';
throw {
message: text + " instead found " + type,
pos: currentToken ? currentToken.pos : this.lexer.char
};
},
// returns token value and incre
consumeToken: function() {
this.index++;
return this.tokens[this.index - 1];
},
matchToken: function(type, index) {
var token = this.tokens[this.index + index];
return (token === undefined && type === '') ||
token && token.type === type;
},
match: function(token1, token2) {
return this.matchToken(token1, 0) &&
(!token2 || this.matchToken(token2, 1));
},
};
return Parser;
});

View File

@ -0,0 +1,258 @@
import {Lexer} from './lexer';
export function Parser(expression) {
this.expression = expression;
this.lexer = new Lexer(expression);
this.tokens = this.lexer.tokenize();
this.index = 0;
}
Parser.prototype = {
getAst: function () {
return this.start();
},
start: function () {
try {
return this.functionCall() || this.metricExpression();
} catch (e) {
return {
type: 'error',
message: e.message,
pos: e.pos
};
}
},
curlyBraceSegment: function() {
if (this.match('identifier', '{') || this.match('{')) {
var curlySegment = "";
while (!this.match('') && !this.match('}')) {
curlySegment += this.consumeToken().value;
}
if (!this.match('}')) {
this.errorMark("Expected closing '}'");
}
curlySegment += this.consumeToken().value;
// if curly segment is directly followed by identifier
// include it in the segment
if (this.match('identifier')) {
curlySegment += this.consumeToken().value;
}
return {
type: 'segment',
value: curlySegment
};
} else {
return null;
}
},
metricSegment: function() {
var curly = this.curlyBraceSegment();
if (curly) {
return curly;
}
if (this.match('identifier') || this.match('number')) {
// hack to handle float numbers in metric segments
var parts = this.consumeToken().value.split('.');
if (parts.length === 2) {
this.tokens.splice(this.index, 0, { type: '.' });
this.tokens.splice(this.index + 1, 0, { type: 'number', value: parts[1] });
}
return {
type: 'segment',
value: parts[0]
};
}
if (!this.match('templateStart')) {
this.errorMark('Expected metric identifier');
}
this.consumeToken();
if (!this.match('identifier')) {
this.errorMark('Expected identifier after templateStart');
}
var node = {
type: 'template',
value: this.consumeToken().value
};
if (!this.match('templateEnd')) {
this.errorMark('Expected templateEnd');
}
this.consumeToken();
return node;
},
metricExpression: function() {
if (!this.match('templateStart') &&
!this.match('identifier') &&
!this.match('number') &&
!this.match('{')) {
return null;
}
var node = {
type: 'metric',
segments: []
};
node.segments.push(this.metricSegment());
while (this.match('.')) {
this.consumeToken();
var segment = this.metricSegment();
if (!segment) {
this.errorMark('Expected metric identifier');
}
node.segments.push(segment);
}
return node;
},
functionCall: function() {
if (!this.match('identifier', '(')) {
return null;
}
var node: any = {
type: 'function',
name: this.consumeToken().value,
};
// consume left parenthesis
this.consumeToken();
node.params = this.functionParameters();
if (!this.match(')')) {
this.errorMark('Expected closing parenthesis');
}
this.consumeToken();
return node;
},
boolExpression: function() {
if (!this.match('bool')) {
return null;
}
return {
type: 'bool',
value: this.consumeToken().value === 'true',
};
},
functionParameters: function () {
if (this.match(')') || this.match('')) {
return [];
}
var param =
this.functionCall() ||
this.numericLiteral() ||
this.seriesRefExpression() ||
this.boolExpression() ||
this.metricExpression() ||
this.stringLiteral();
if (!this.match(',')) {
return [param];
}
this.consumeToken();
return [param].concat(this.functionParameters());
},
seriesRefExpression: function() {
if (!this.match('identifier')) {
return null;
}
var value = this.tokens[this.index].value;
if (!value.match(/\#[A-Z]/)) {
return null;
}
var token = this.consumeToken();
return {
type: 'series-ref',
value: token.value
};
},
numericLiteral: function () {
if (!this.match('number')) {
return null;
}
return {
type: 'number',
value: parseFloat(this.consumeToken().value)
};
},
stringLiteral: function () {
if (!this.match('string')) {
return null;
}
var token = this.consumeToken();
if (token.isUnclosed) {
throw { message: 'Unclosed string parameter', pos: token.pos };
}
return {
type: 'string',
value: token.value
};
},
errorMark: function(text) {
var currentToken = this.tokens[this.index];
var type = currentToken ? currentToken.type : 'end of string';
throw {
message: text + " instead found " + type,
pos: currentToken ? currentToken.pos : this.lexer.char
};
},
// returns token value and incre
consumeToken: function() {
this.index++;
return this.tokens[this.index - 1];
},
matchToken: function(type, index) {
var token = this.tokens[this.index + index];
return (token === undefined && type === '') ||
token && token.type === type;
},
match: function(token1, token2) {
return this.matchToken(token1, 0) &&
(!token2 || this.matchToken(token2, 1));
},
};

View File

@ -1,14 +1,14 @@
<div class="editor-row">
<div class="editor-option">
<label class="small">Graphite target expression</label>
<input type="text" class="span10" ng-model='annotation.target' placeholder=""></input>
<input type="text" class="span10" ng-model='ctrl.annotation.target' placeholder=""></input>
</div>
</div>
<div class="editor-row">
<div class="editor-option">
<label class="small">Graphite event tags</label>
<input type="text" ng-model='annotation.tags' placeholder=""></input>
<input type="text" ng-model='ctrl.annotation.tags' placeholder=""></input>
</div>
</div>

View File

@ -1,2 +1,3 @@
<datasource-http-settings></datasource-http-settings>
<datasource-http-settings current="ctrl.current">
</datasource-http-settings>

View File

@ -1,73 +1,21 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li ng-show="parserError" class="tight-form-item">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="target.datasource">
<em>{{target.datasource}}</em>
</li>
<li class="tight-form-item">
<a class="pointer" tabindex="1" ng-click="toggleEditorMode()">
<i class="fa fa-pencil"></i>
</a>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1" ng-click="toggleEditorMode()">
Switch editor mode
</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.duplicateDataQuery(target)">Duplicate</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index-1)">Move up</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveDataQuery($index, $index+1)">Move down</a>
</li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="ctrl.removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<query-editor-row ctrl="ctrl">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="target.hide = !target.hide; panelCtrl.refresh();" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<li class="tight-form-flex-wrapper" ng-show="ctrl.target.textEditor">
<input type="text" class="tight-form-clear-input" style="width: 100%;" ng-model="ctrl.target.target" give-focus="ctrl.target.textEditor" spellcheck='false' ng-model-onblur ng-change="ctrl.targetTextChanged()"></input>
</li>
<span style="display: block; overflow: hidden;">
<input type="text" class="tight-form-clear-input" style="width: 100%;" ng-model="target.target" give-focus="target.textEditor" spellcheck='false' ng-model-onblur ng-change="panelCtrl.getData()" ng-show="target.textEditor"></input>
</span>
<li ng-hide-start="ctrl.target.textEditor"></li>
<ul class="tight-form-list" role="menu" ng-hide="target.textEditor">
<li ng-repeat="segment in segments" role="menuitem">
<metric-segment segment="segment" get-options="getAltSegments($index)" on-change="segmentValueChanged(segment, $index)"></metric-segment>
</li>
<li ng-repeat="func in functions">
<span graphite-func-editor class="tight-form-item tight-form-func">
</span>
</li>
<li class="dropdown" graphite-add-func>
</li>
</ul>
<div class="clearfix"></div>
</div>
<li ng-repeat="segment in ctrl.segments" role="menuitem">
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
</li>
<li ng-repeat="func in ctrl.functions">
<span graphite-func-editor class="tight-form-item tight-form-func">
</span>
</li>
<li class="dropdown" graphite-add-func>
</li>
<li ng-hide-end></li>
</query-editor-row>

View File

@ -1,5 +1,4 @@
<section class="grafana-metric-options">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item tight-form-item-icon">
@ -11,7 +10,7 @@
<li>
<input type="text"
class="input-mini tight-form-input"
ng-model="ctrl.panel.cacheTimeout"
ng-model="ctrl.panelCtrl.panel.cacheTimeout"
bs-tooltip="'Graphite parameter to override memcache default timeout (unit is seconds)'"
data-placement="right"
spellcheck='false'
@ -23,10 +22,10 @@
<li>
<input type="text"
class="input-mini tight-form-input"
ng-model="ctrl.panel.maxDataPoints"
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.refresh()"
ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
spellcheck='false'
placeholder="auto"></input>
</li>
@ -39,27 +38,27 @@
<i class="fa fa-info-circle"></i>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
shorter legend names
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
series as parameters
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
stacking
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
templating
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(5)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
max data points
</a>
</li>
@ -71,7 +70,7 @@
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span8" ng-if="ctrl.editorHelpIndex === 1">
<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>
@ -81,7 +80,7 @@
</ul>
</div>
<div class="grafana-info-box span8" ng-if="ctrl.editorHelpIndex === 2">
<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>
@ -99,7 +98,7 @@
</ul>
</div>
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 3">
<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>
@ -107,7 +106,7 @@
</ul>
</div>
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 4">
<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>
@ -116,7 +115,7 @@
</ul>
</div>
<div class="grafana-info-box span6" ng-if="ctrl.editorHelpIndex === 5">
<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>

View File

@ -1,292 +0,0 @@
define([
'angular',
'lodash',
'app/core/config',
'./gfunc',
'./parser'
],
function (angular, _, config, gfunc, Parser) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('GraphiteQueryCtrl', function($scope, uiSegmentSrv, templateSrv) {
var panelCtrl = $scope.panelCtrl = $scope.ctrl;
var datasource = $scope.datasource;
$scope.init = function() {
if ($scope.target) {
$scope.target.target = $scope.target.target || '';
parseTarget();
}
};
$scope.toggleEditorMode = function() {
$scope.target.textEditor = !$scope.target.textEditor;
parseTarget();
};
// The way parsing and the target editor works needs
// to be rewritten to handle functions that take multiple series
function parseTarget() {
$scope.functions = [];
$scope.segments = [];
delete $scope.parserError;
if ($scope.target.textEditor) {
return;
}
var parser = new Parser($scope.target.target);
var astNode = parser.getAst();
if (astNode === null) {
checkOtherSegments(0);
return;
}
if (astNode.type === 'error') {
$scope.parserError = astNode.message + " at position: " + astNode.pos;
$scope.target.textEditor = true;
return;
}
try {
parseTargeRecursive(astNode);
}
catch (err) {
console.log('error parsing target:', err.message);
$scope.parserError = err.message;
$scope.target.textEditor = true;
}
checkOtherSegments($scope.segments.length - 1);
}
function addFunctionParameter(func, value, index, shiftBack) {
if (shiftBack) {
index = Math.max(index - 1, 0);
}
func.params[index] = value;
}
function parseTargeRecursive(astNode, func, index) {
if (astNode === null) {
return null;
}
switch(astNode.type) {
case 'function':
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
_.each(astNode.params, function(param, index) {
parseTargeRecursive(param, innerFunc, index);
});
innerFunc.updateText();
$scope.functions.push(innerFunc);
break;
case 'series-ref':
addFunctionParameter(func, astNode.value, index, $scope.segments.length > 0);
break;
case 'bool':
case 'string':
case 'number':
if ((index-1) >= func.def.params.length) {
throw { message: 'invalid number of parameters to method ' + func.def.name };
}
addFunctionParameter(func, astNode.value, index, true);
break;
case 'metric':
if ($scope.segments.length > 0) {
if (astNode.segments.length !== 1) {
throw { message: 'Multiple metric params not supported, use text editor.' };
}
addFunctionParameter(func, astNode.segments[0].value, index, true);
break;
}
$scope.segments = _.map(astNode.segments, function(segment) {
return uiSegmentSrv.newSegment(segment);
});
}
}
function getSegmentPathUpTo(index) {
var arr = $scope.segments.slice(0, index);
return _.reduce(arr, function(result, segment) {
return result ? (result + "." + segment.value) : segment.value;
}, "");
}
function checkOtherSegments(fromIndex) {
if (fromIndex === 0) {
$scope.segments.push(uiSegmentSrv.newSelectMetric());
return;
}
var path = getSegmentPathUpTo(fromIndex + 1);
return datasource.metricFindQuery(path)
.then(function(segments) {
if (segments.length === 0) {
if (path !== '') {
$scope.segments = $scope.segments.splice(0, fromIndex);
$scope.segments.push(uiSegmentSrv.newSelectMetric());
}
} else if (segments[0].expandable) {
if ($scope.segments.length === fromIndex) {
$scope.segments.push(uiSegmentSrv.newSelectMetric());
}
else {
return checkOtherSegments(fromIndex + 1);
}
}
})
.then(null, function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
});
}
function setSegmentFocus(segmentIndex) {
_.each($scope.segments, function(segment, index) {
segment.focus = segmentIndex === index;
});
}
function wrapFunction(target, func) {
return func.render(target);
}
$scope.getAltSegments = function (index) {
var query = index === 0 ? '*' : getSegmentPathUpTo(index) + '.*';
return datasource.metricFindQuery(query).then(function(segments) {
var altSegments = _.map(segments, function(segment) {
return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (altSegments.length === 0) { return altSegments; }
// add template variables
_.each(templateSrv.variables, function(variable) {
altSegments.unshift(uiSegmentSrv.newSegment({
type: 'template',
value: '$' + variable.name,
expandable: true,
}));
});
// add wildcard option
altSegments.unshift(uiSegmentSrv.newSegment('*'));
return altSegments;
})
.then(null, function(err) {
$scope.parserError = err.message || 'Failed to issue metric query';
return [];
});
};
$scope.segmentValueChanged = function (segment, segmentIndex) {
delete $scope.parserError;
if ($scope.functions.length > 0 && $scope.functions[0].def.fake) {
$scope.functions = [];
}
if (segment.expandable) {
return checkOtherSegments(segmentIndex + 1).then(function() {
setSegmentFocus(segmentIndex + 1);
$scope.targetChanged();
});
}
else {
$scope.segments = $scope.segments.splice(0, segmentIndex + 1);
}
setSegmentFocus(segmentIndex + 1);
$scope.targetChanged();
};
$scope.targetTextChanged = function() {
parseTarget();
panelCtrl.refresh();
};
$scope.targetChanged = function() {
if ($scope.parserError) {
return;
}
var oldTarget = $scope.target.target;
var target = getSegmentPathUpTo($scope.segments.length);
$scope.target.target = _.reduce($scope.functions, wrapFunction, target);
if ($scope.target.target !== oldTarget) {
if ($scope.segments[$scope.segments.length - 1].value !== 'select metric') {
panelCtrl.refresh();
}
}
};
$scope.removeFunction = function(func) {
$scope.functions = _.without($scope.functions, func);
$scope.targetChanged();
};
$scope.addFunction = function(funcDef) {
var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
newFunc.added = true;
$scope.functions.push(newFunc);
$scope.moveAliasFuncLast();
$scope.smartlyHandleNewAliasByNode(newFunc);
if ($scope.segments.length === 1 && $scope.segments[0].fake) {
$scope.segments = [];
}
if (!newFunc.params.length && newFunc.added) {
$scope.targetChanged();
}
};
$scope.moveAliasFuncLast = function() {
var aliasFunc = _.find($scope.functions, function(func) {
return func.def.name === 'alias' ||
func.def.name === 'aliasByNode' ||
func.def.name === 'aliasByMetric';
});
if (aliasFunc) {
$scope.functions = _.without($scope.functions, aliasFunc);
$scope.functions.push(aliasFunc);
}
};
$scope.smartlyHandleNewAliasByNode = function(func) {
if (func.def.name !== 'aliasByNode') {
return;
}
for(var i = 0; i < $scope.segments.length; i++) {
if ($scope.segments[i].value.indexOf('*') >= 0) {
func.params[0] = i;
func.added = false;
$scope.targetChanged();
return;
}
}
};
$scope.toggleMetricOptions = function() {
$scope.panel.metricOptionsEnabled = !$scope.panel.metricOptionsEnabled;
if (!$scope.panel.metricOptionsEnabled) {
delete $scope.panel.cacheTimeout;
}
};
$scope.init();
});
});

View File

@ -0,0 +1,276 @@
///<reference path="../../../headers/common.d.ts" />
import './add_graphite_func';
import './func_editor';
import angular from 'angular';
import _ from 'lodash';
import moment from 'moment';
import gfunc from './gfunc';
import {Parser} from './parser';
import {QueryCtrl} from 'app/features/panel/panel';
export class GraphiteQueryCtrl extends QueryCtrl {
static templateUrl = 'public/app/plugins/datasource/graphite/partials/query.editor.html';
functions: any[];
segments: any[];
/** @ngInject **/
constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
super($scope, $injector);
if (this.target) {
this.target.target = this.target.target || '';
this.parseTarget();
}
}
toggleEditorMode() {
this.target.textEditor = !this.target.textEditor;
this.parseTarget();
}
parseTarget() {
this.functions = [];
this.segments = [];
this.error = null;
if (this.target.textEditor) {
return;
}
var parser = new Parser(this.target.target);
var astNode = parser.getAst();
if (astNode === null) {
this.checkOtherSegments(0);
return;
}
if (astNode.type === 'error') {
this.error = astNode.message + " at position: " + astNode.pos;
this.target.textEditor = true;
return;
}
try {
this.parseTargeRecursive(astNode, null, 0);
} catch (err) {
console.log('error parsing target:', err.message);
this.error = err.message;
this.target.textEditor = true;
}
this.checkOtherSegments(this.segments.length - 1);
}
addFunctionParameter(func, value, index, shiftBack) {
if (shiftBack) {
index = Math.max(index - 1, 0);
}
func.params[index] = value;
}
parseTargeRecursive(astNode, func, index) {
if (astNode === null) {
return null;
}
switch (astNode.type) {
case 'function':
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
_.each(astNode.params, (param, index) => {
this.parseTargeRecursive(param, innerFunc, index);
});
innerFunc.updateText();
this.functions.push(innerFunc);
break;
case 'series-ref':
this.addFunctionParameter(func, astNode.value, index, this.segments.length > 0);
break;
case 'bool':
case 'string':
case 'number':
if ((index-1) >= func.def.params.length) {
throw { message: 'invalid number of parameters to method ' + func.def.name };
}
this.addFunctionParameter(func, astNode.value, index, true);
break;
case 'metric':
if (this.segments.length > 0) {
if (astNode.segments.length !== 1) {
throw { message: 'Multiple metric params not supported, use text editor.' };
}
this.addFunctionParameter(func, astNode.segments[0].value, index, true);
break;
}
this.segments = _.map(astNode.segments, segment => {
return this.uiSegmentSrv.newSegment(segment);
});
}
}
getSegmentPathUpTo(index) {
var arr = this.segments.slice(0, index);
return _.reduce(arr, function(result, segment) {
return result ? (result + "." + segment.value) : segment.value;
}, "");
}
checkOtherSegments(fromIndex) {
if (fromIndex === 0) {
this.segments.push(this.uiSegmentSrv.newSelectMetric());
return;
}
var path = this.getSegmentPathUpTo(fromIndex + 1);
return this.datasource.metricFindQuery(path).then(segments => {
if (segments.length === 0) {
if (path !== '') {
this.segments = this.segments.splice(0, fromIndex);
this.segments.push(this.uiSegmentSrv.newSelectMetric());
}
} else if (segments[0].expandable) {
if (this.segments.length === fromIndex) {
this.segments.push(this.uiSegmentSrv.newSelectMetric());
} else {
return this.checkOtherSegments(fromIndex + 1);
}
}
}).catch(err => {
this.error = err.message || 'Failed to issue metric query';
});
}
setSegmentFocus(segmentIndex) {
_.each(this.segments, (segment, index) => {
segment.focus = segmentIndex === index;
});
}
wrapFunction(target, func) {
return func.render(target);
}
getAltSegments(index) {
var query = index === 0 ? '*' : this.getSegmentPathUpTo(index) + '.*';
return this.datasource.metricFindQuery(query).then(segments => {
var altSegments = _.map(segments, segment => {
return this.uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (altSegments.length === 0) { return altSegments; }
// add template variables
_.each(this.templateSrv.variables, variable => {
altSegments.unshift(this.uiSegmentSrv.newSegment({
type: 'template',
value: '$' + variable.name,
expandable: true,
}));
});
// add wildcard option
altSegments.unshift(this.uiSegmentSrv.newSegment('*'));
return altSegments;
}).catch(err => {
this.error = err.message || 'Failed to issue metric query';
return [];
});
}
segmentValueChanged(segment, segmentIndex) {
this.error = null;
if (this.functions.length > 0 && this.functions[0].def.fake) {
this.functions = [];
}
if (segment.expandable) {
return this.checkOtherSegments(segmentIndex + 1).then(() => {
this.setSegmentFocus(segmentIndex + 1);
this.targetChanged();
});
} else {
this.segments = this.segments.splice(0, segmentIndex + 1);
}
this.setSegmentFocus(segmentIndex + 1);
this.targetChanged();
}
targetTextChanged() {
this.parseTarget();
this.panelCtrl.refresh();
}
targetChanged() {
if (this.error) {
return;
}
var oldTarget = this.target.target;
var target = this.getSegmentPathUpTo(this.segments.length);
this.target.target = _.reduce(this.functions, this.wrapFunction, target);
if (this.target.target !== oldTarget) {
if (this.segments[this.segments.length - 1].value !== 'select metric') {
this.panelCtrl.refresh();
}
}
}
removeFunction(func) {
this.functions = _.without(this.functions, func);
this.targetChanged();
}
addFunction(funcDef) {
var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
newFunc.added = true;
this.functions.push(newFunc);
this.moveAliasFuncLast();
this.smartlyHandleNewAliasByNode(newFunc);
if (this.segments.length === 1 && this.segments[0].fake) {
this.segments = [];
}
if (!newFunc.params.length && newFunc.added) {
this.targetChanged();
}
}
moveAliasFuncLast() {
var aliasFunc = _.find(this.functions, function(func) {
return func.def.name === 'alias' ||
func.def.name === 'aliasByNode' ||
func.def.name === 'aliasByMetric';
});
if (aliasFunc) {
this.functions = _.without(this.functions, aliasFunc);
this.functions.push(aliasFunc);
}
}
smartlyHandleNewAliasByNode(func) {
if (func.def.name !== 'aliasByNode') {
return;
}
for (var i = 0; i < this.segments.length; i++) {
if (this.segments[i].value.indexOf('*') >= 0) {
func.params[0] = i;
func.added = false;
this.targetChanged();
return;
}
}
}
}

View File

@ -1,7 +1,7 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import helpers from 'test/specs/helpers';
import Datasource from "../datasource";
import {GraphiteDatasource} from "../datasource";
describe('graphiteDatasource', function() {
var ctx = new helpers.ServiceTestContext();
@ -18,7 +18,7 @@ describe('graphiteDatasource', function() {
}));
beforeEach(function() {
ctx.ds = ctx.$injector.instantiate(Datasource, {instanceSettings: instanceSettings});
ctx.ds = ctx.$injector.instantiate(GraphiteDatasource, {instanceSettings: instanceSettings});
});
describe('When querying influxdb with one target using query editor target spec', function() {

Some files were not shown because too many files have changed in this diff Show More