mirror of
https://github.com/grafana/grafana.git
synced 2026-01-08 10:52:02 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8aa16673e | ||
|
|
0d821d07ad | ||
|
|
f7092fa6b0 | ||
|
|
9ca86f1b0e | ||
|
|
dc70298210 | ||
|
|
d20c7260b3 | ||
|
|
a54fa3858e | ||
|
|
7be0716752 | ||
|
|
cb1eedb5f7 | ||
|
|
0d8c7573f3 | ||
|
|
c9591f8a8c | ||
|
|
6c3202b1b6 | ||
|
|
5daf842431 | ||
|
|
487a8585c6 | ||
|
|
20a47ed3d6 | ||
|
|
72e60346bc | ||
|
|
f213f664ce | ||
|
|
5b9116bf80 | ||
|
|
3891b82443 | ||
|
|
7ddccdba08 | ||
|
|
02b4cf392d | ||
|
|
112fa2b8b9 | ||
|
|
e8e8b014f6 | ||
|
|
02a3e11708 | ||
|
|
b0f91f3a3e | ||
|
|
99a8bf2195 |
@@ -134,6 +134,16 @@ Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}`
|
||||
|
||||
Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod`
|
||||
|
||||
It is also possible to resolve the name of the Monitored Resource Type.
|
||||
|
||||
| Alias Pattern Format | Description | Example Result |
|
||||
| ------------------------ | ------------------------------------------------| ---------------- |
|
||||
| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` |
|
||||
|
||||
Example Alias By: `{{resource.type}} - {{metric.type}}`
|
||||
|
||||
Example Result: `gce_instance - compute.googleapis.com/instance/cpu/usage_time`
|
||||
|
||||
## Templating
|
||||
|
||||
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"company": "Grafana Labs"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "5.3.1",
|
||||
"version": "5.3.3",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
|
||||
@@ -25,7 +25,7 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
|
||||
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
|
||||
RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates curl && \
|
||||
apt-get autoremove -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -185,7 +185,9 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
|
||||
|
||||
if ldapUser.isMemberOf(group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
if extUser.IsGrafanaAdmin == nil || *extUser.IsGrafanaAdmin == false {
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ var New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardia
|
||||
user: user,
|
||||
dashId: dashId,
|
||||
orgId: orgId,
|
||||
log: log.New("guardians.dashboard"),
|
||||
log: log.New("dashboard.permissions"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,15 +66,30 @@ func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
|
||||
|
||||
func (g *dashboardGuardianImpl) HasPermission(permission m.PermissionType) (bool, error) {
|
||||
if g.user.OrgRole == m.ROLE_ADMIN {
|
||||
return true, nil
|
||||
return g.logHasPermissionResult(permission, true, nil)
|
||||
}
|
||||
|
||||
acl, err := g.GetAcl()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return g.logHasPermissionResult(permission, false, err)
|
||||
}
|
||||
|
||||
return g.checkAcl(permission, acl)
|
||||
result, err := g.checkAcl(permission, acl)
|
||||
return g.logHasPermissionResult(permission, result, err)
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) logHasPermissionResult(permission m.PermissionType, hasPermission bool, err error) (bool, error) {
|
||||
if err != nil {
|
||||
return hasPermission, err
|
||||
}
|
||||
|
||||
if hasPermission {
|
||||
g.log.Debug("User granted access to execute action", "userId", g.user.UserId, "orgId", g.orgId, "uname", g.user.Login, "dashId", g.dashId, "action", permission)
|
||||
} else {
|
||||
g.log.Debug("User denied access to execute action", "userId", g.user.UserId, "orgId", g.orgId, "uname", g.user.Login, "dashId", g.dashId, "action", permission)
|
||||
}
|
||||
|
||||
return hasPermission, err
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {
|
||||
|
||||
@@ -42,7 +42,8 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
|
||||
|
||||
cmdArgs := []string{
|
||||
"--ignore-ssl-errors=true",
|
||||
"--web-security=false",
|
||||
"--web-security=true",
|
||||
"--local-url-access=false",
|
||||
phantomDebugArg,
|
||||
scriptPath,
|
||||
fmt.Sprintf("url=%v", url),
|
||||
|
||||
@@ -320,13 +320,18 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
||||
"DELETE FROM dashboard WHERE id = ?",
|
||||
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
||||
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard WHERE folder_id = ?",
|
||||
"DELETE FROM annotation WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
|
||||
}
|
||||
|
||||
if dashboard.IsFolder {
|
||||
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
|
||||
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
_, err := sess.Exec(sql, dashboard.Id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,17 +13,30 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
||||
Convey("Testing Dashboard provisioning", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
saveDashboardCmd := &models.SaveDashboardCommand{
|
||||
folderCmd := &models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
FolderId: 0,
|
||||
IsFolder: false,
|
||||
IsFolder: true,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dashboard",
|
||||
}),
|
||||
}
|
||||
|
||||
Convey("Saving dashboards with extras", func() {
|
||||
err := SaveDashboard(folderCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
saveDashboardCmd := &models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
IsFolder: false,
|
||||
FolderId: folderCmd.Result.Id,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dashboard",
|
||||
}),
|
||||
}
|
||||
|
||||
Convey("Saving dashboards with provisioning meta data", func() {
|
||||
now := time.Now()
|
||||
|
||||
cmd := &models.SaveProvisionedDashboardCommand{
|
||||
@@ -67,6 +80,21 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Deleteing folder should delete provision meta data", func() {
|
||||
deleteCmd := &models.DeleteDashboardCommand{
|
||||
Id: folderCmd.Result.Id,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
So(DeleteDashboard(deleteCmd), ShouldBeNil)
|
||||
|
||||
query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
|
||||
|
||||
err = GetProvisionedDataByDashboardId(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,9 +86,10 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
result := &tsdb.Response{
|
||||
results := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
resultChan := make(chan *tsdb.QueryResult, len(queryContext.Queries))
|
||||
|
||||
eg, ectx := errgroup.WithContext(ctx)
|
||||
|
||||
@@ -102,10 +103,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
RefId := queryContext.Queries[i].RefId
|
||||
query, err := parseQuery(queryContext.Queries[i].Model)
|
||||
if err != nil {
|
||||
result.Results[RefId] = &tsdb.QueryResult{
|
||||
results.Results[RefId] = &tsdb.QueryResult{
|
||||
Error: err,
|
||||
}
|
||||
return result, nil
|
||||
return results, nil
|
||||
}
|
||||
query.RefId = RefId
|
||||
|
||||
@@ -118,10 +119,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
}
|
||||
|
||||
if query.Id == "" && query.Expression != "" {
|
||||
result.Results[query.RefId] = &tsdb.QueryResult{
|
||||
results.Results[query.RefId] = &tsdb.QueryResult{
|
||||
Error: fmt.Errorf("Invalid query: id should be set if using expression"),
|
||||
}
|
||||
return result, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
@@ -130,12 +131,13 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
result.Results[query.RefId] = &tsdb.QueryResult{
|
||||
resultChan <- &tsdb.QueryResult{
|
||||
RefId: query.RefId,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
result.Results[queryRes.RefId] = queryRes
|
||||
resultChan <- queryRes
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -149,10 +151,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
return err
|
||||
}
|
||||
for _, queryRes := range queryResponses {
|
||||
result.Results[queryRes.RefId] = queryRes
|
||||
if err != nil {
|
||||
result.Results[queryRes.RefId].Error = err
|
||||
queryRes.Error = err
|
||||
}
|
||||
resultChan <- queryRes
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -162,8 +164,12 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
close(resultChan)
|
||||
for result := range resultChan {
|
||||
results.Results[result.RefId] = result
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
|
||||
|
||||
@@ -337,11 +337,21 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver
|
||||
func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery) error {
|
||||
metricLabels := make(map[string][]string)
|
||||
resourceLabels := make(map[string][]string)
|
||||
var resourceTypes []string
|
||||
|
||||
for _, series := range data.TimeSeries {
|
||||
if !containsLabel(resourceTypes, series.Resource.Type) {
|
||||
resourceTypes = append(resourceTypes, series.Resource.Type)
|
||||
}
|
||||
}
|
||||
|
||||
for _, series := range data.TimeSeries {
|
||||
points := make([]tsdb.TimePoint, 0)
|
||||
|
||||
defaultMetricName := series.Metric.Type
|
||||
if len(resourceTypes) > 1 {
|
||||
defaultMetricName += " " + series.Resource.Type
|
||||
}
|
||||
|
||||
for key, value := range series.Metric.Labels {
|
||||
if !containsLabel(metricLabels[key], value) {
|
||||
@@ -385,7 +395,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
||||
points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000))
|
||||
}
|
||||
|
||||
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query)
|
||||
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query)
|
||||
|
||||
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
|
||||
Name: metricName,
|
||||
@@ -411,7 +421,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
||||
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
|
||||
additionalLabels := map[string]string{"bucket": bucketBound}
|
||||
buckets[i] = &tsdb.TimeSeries{
|
||||
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
|
||||
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
|
||||
Points: make([]tsdb.TimePoint, 0),
|
||||
}
|
||||
if maxKey < i {
|
||||
@@ -427,7 +437,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
||||
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
|
||||
additionalLabels := map[string]string{"bucket": bucketBound}
|
||||
buckets[i] = &tsdb.TimeSeries{
|
||||
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
|
||||
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
|
||||
Points: make([]tsdb.TimePoint, 0),
|
||||
}
|
||||
}
|
||||
@@ -442,6 +452,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
||||
queryRes.Meta.Set("resourceLabels", resourceLabels)
|
||||
queryRes.Meta.Set("metricLabels", metricLabels)
|
||||
queryRes.Meta.Set("groupBys", query.GroupBys)
|
||||
queryRes.Meta.Set("resourceTypes", resourceTypes)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -455,7 +466,7 @@ func containsLabel(labels []string, newLabel string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func formatLegendKeys(metricType string, defaultMetricName string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
|
||||
func formatLegendKeys(metricType string, defaultMetricName string, resourceType string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
|
||||
if query.AliasBy == "" {
|
||||
return defaultMetricName
|
||||
}
|
||||
@@ -469,6 +480,10 @@ func formatLegendKeys(metricType string, defaultMetricName string, metricLabels
|
||||
return []byte(metricType)
|
||||
}
|
||||
|
||||
if metaPartName == "resource.type" && resourceType != "" {
|
||||
return []byte(resourceType)
|
||||
}
|
||||
|
||||
metricPart := replaceWithMetricPart(metaPartName, metricType)
|
||||
|
||||
if metricPart != nil {
|
||||
|
||||
@@ -81,7 +81,7 @@ function setViewModeBodyClass(body, mode, sidemenuOpen: boolean) {
|
||||
break;
|
||||
}
|
||||
// 1 & true for legacy states
|
||||
case 1:
|
||||
case '1':
|
||||
case true: {
|
||||
body.removeClass('sidemenu-open');
|
||||
body.addClass('view-mode--kiosk');
|
||||
@@ -169,16 +169,16 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
const search = $location.search();
|
||||
|
||||
if (options && options.exit) {
|
||||
search.kiosk = 1;
|
||||
search.kiosk = '1';
|
||||
}
|
||||
|
||||
switch (search.kiosk) {
|
||||
case 'tv': {
|
||||
search.kiosk = 1;
|
||||
search.kiosk = true;
|
||||
appEvents.emit('alert-success', ['Press ESC to exit Kiosk mode']);
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
case '1':
|
||||
case true: {
|
||||
delete search.kiosk;
|
||||
break;
|
||||
|
||||
@@ -103,7 +103,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
||||
$scope.$apply(() => {
|
||||
$scope.handleEvent({ $event: { name: 'get-param-options' } }).then(result => {
|
||||
const dynamicOptions = _.map(result, op => {
|
||||
return op.value;
|
||||
return _.escape(op.value);
|
||||
});
|
||||
callback(dynamicOptions);
|
||||
});
|
||||
@@ -117,6 +117,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
||||
minLength: 0,
|
||||
items: 1000,
|
||||
updater: value => {
|
||||
value = _.unescape(value);
|
||||
setTimeout(() => {
|
||||
inputBlur.call($input[0], paramIndex);
|
||||
}, 0);
|
||||
|
||||
@@ -109,12 +109,12 @@ export function sqlPartEditorDirective($compile, templateSrv) {
|
||||
$scope.$apply(() => {
|
||||
$scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(result => {
|
||||
const dynamicOptions = _.map(result, op => {
|
||||
return op.value;
|
||||
return _.escape(op.value);
|
||||
});
|
||||
|
||||
// add current value to dropdown if it's not in dynamicOptions
|
||||
if (_.indexOf(dynamicOptions, part.params[paramIndex]) === -1) {
|
||||
dynamicOptions.unshift(part.params[paramIndex]);
|
||||
dynamicOptions.unshift(_.escape(part.params[paramIndex]));
|
||||
}
|
||||
|
||||
callback(dynamicOptions);
|
||||
@@ -129,6 +129,7 @@ export function sqlPartEditorDirective($compile, templateSrv) {
|
||||
minLength: 0,
|
||||
items: 1000,
|
||||
updater: value => {
|
||||
value = _.unescape(value);
|
||||
if (value === part.params[paramIndex]) {
|
||||
clearTimeout(cancelBlur);
|
||||
$input.focus();
|
||||
|
||||
@@ -3,7 +3,7 @@ import $ from 'jquery';
|
||||
import coreModule from '../core_module';
|
||||
|
||||
/** @ngInject */
|
||||
export function metricSegment($compile, $sce) {
|
||||
export function metricSegment($compile, $sce, templateSrv) {
|
||||
const inputTemplate =
|
||||
'<input type="text" data-provide="typeahead" ' +
|
||||
' class="gf-form-input input-medium"' +
|
||||
@@ -41,13 +41,11 @@ export function metricSegment($compile, $sce) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = _.unescape(value);
|
||||
|
||||
$scope.$apply(() => {
|
||||
const selected = _.find($scope.altSegments, { value: value });
|
||||
if (selected) {
|
||||
segment.value = selected.value;
|
||||
segment.html = selected.html || selected.value;
|
||||
segment.html = selected.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(selected.value));
|
||||
segment.fake = false;
|
||||
segment.expandable = selected.expandable;
|
||||
|
||||
@@ -56,7 +54,7 @@ export function metricSegment($compile, $sce) {
|
||||
}
|
||||
} else if (segment.custom !== 'false') {
|
||||
segment.value = value;
|
||||
segment.html = $sce.trustAsHtml(value);
|
||||
segment.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(value));
|
||||
segment.expandable = true;
|
||||
segment.fake = false;
|
||||
}
|
||||
@@ -95,7 +93,7 @@ export function metricSegment($compile, $sce) {
|
||||
// add custom values
|
||||
if (segment.custom !== 'false') {
|
||||
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
|
||||
options.unshift(segment.value);
|
||||
options.unshift(_.escape(segment.value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +103,7 @@ export function metricSegment($compile, $sce) {
|
||||
};
|
||||
|
||||
$scope.updater = value => {
|
||||
value = _.unescape(value);
|
||||
if (value === segment.value) {
|
||||
clearTimeout(cancelBlur);
|
||||
$input.focus();
|
||||
|
||||
@@ -429,6 +429,11 @@ describe('templateSrv', () => {
|
||||
name: 'period',
|
||||
current: { value: '$__auto_interval_interval', text: 'auto' },
|
||||
},
|
||||
{
|
||||
type: 'textbox',
|
||||
name: 'empty_on_init',
|
||||
current: { value: '', text: '' },
|
||||
},
|
||||
]);
|
||||
_templateSrv.setGrafanaVariable('$__auto_interval_interval', '13m');
|
||||
_templateSrv.updateTemplateData();
|
||||
@@ -438,6 +443,11 @@ describe('templateSrv', () => {
|
||||
const target = _templateSrv.replaceWithText('Server: $server, period: $period');
|
||||
expect(target).toBe('Server: All, period: 13m');
|
||||
});
|
||||
|
||||
it('should replace empty string-values with an empty string', () => {
|
||||
const target = _templateSrv.replaceWithText('Hello $empty_on_init');
|
||||
expect(target).toBe('Hello ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('built in interval variables', () => {
|
||||
|
||||
@@ -22,6 +22,11 @@ describe('containsVariable', () => {
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it with [[var:option]] syntax', () => {
|
||||
const contains = containsVariable('this.[[test:csv]].filters', 'test');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it when part of segment', () => {
|
||||
const contains = containsVariable('metrics.$env.$group-*', 'group');
|
||||
expect(contains).toBe(true);
|
||||
@@ -36,6 +41,16 @@ describe('containsVariable', () => {
|
||||
const contains = containsVariable('asd', 'asd2.$env', 'env');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it with ${var} syntax', () => {
|
||||
const contains = containsVariable('this.${test}.filters', 'test');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
|
||||
it('should find it with ${var:option} syntax', () => {
|
||||
const contains = containsVariable('this.${test:csv}.filters', 'test');
|
||||
expect(contains).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import _ from 'lodash';
|
||||
import { variableRegex } from 'app/features/templating/variable';
|
||||
|
||||
function luceneEscape(value) {
|
||||
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');
|
||||
@@ -8,13 +9,7 @@ function luceneEscape(value) {
|
||||
export class TemplateSrv {
|
||||
variables: any[];
|
||||
|
||||
/*
|
||||
* This regex matches 3 types of variable reference with an optional format specifier
|
||||
* \$(\w+) $var1
|
||||
* \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
|
||||
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
|
||||
*/
|
||||
private regex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g;
|
||||
private regex = variableRegex;
|
||||
private index = {};
|
||||
private grafanaVariables = {};
|
||||
private builtIns = {};
|
||||
@@ -30,17 +25,14 @@ export class TemplateSrv {
|
||||
}
|
||||
|
||||
updateTemplateData() {
|
||||
this.index = {};
|
||||
const existsOrEmpty = value => value || value === '';
|
||||
|
||||
for (let i = 0; i < this.variables.length; i++) {
|
||||
const variable = this.variables[i];
|
||||
|
||||
if (!variable.current || (!variable.current.isNone && !variable.current.value)) {
|
||||
continue;
|
||||
this.index = this.variables.reduce((acc, currentValue) => {
|
||||
if (currentValue.current && !currentValue.current.isNone && existsOrEmpty(currentValue.current.value)) {
|
||||
acc[currentValue.name] = currentValue;
|
||||
}
|
||||
|
||||
this.index[variable.name] = variable;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
variableInitialized(variable) {
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { assignModelProperties } from 'app/core/utils/model_utils';
|
||||
|
||||
/*
|
||||
* This regex matches 3 types of variable reference with an optional format specifier
|
||||
* \$(\w+) $var1
|
||||
* \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]]
|
||||
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3}
|
||||
*/
|
||||
export const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?::(\w+))?}/g;
|
||||
|
||||
// Helper function since lastIndex is not reset
|
||||
export const variableRegexExec = (variableString: string) => {
|
||||
variableRegex.lastIndex = 0;
|
||||
return variableRegex.exec(variableString);
|
||||
};
|
||||
|
||||
export interface Variable {
|
||||
setValue(option);
|
||||
updateOptions();
|
||||
@@ -14,15 +27,16 @@ export let variableTypes = {};
|
||||
export { assignModelProperties };
|
||||
|
||||
export function containsVariable(...args: any[]) {
|
||||
let variableName = args[args.length - 1];
|
||||
let str = args[0] || '';
|
||||
const variableName = args[args.length - 1];
|
||||
const variableString = args.slice(0, -1).join(' ');
|
||||
const matches = variableString.match(variableRegex);
|
||||
const isMatchingVariable =
|
||||
matches !== null
|
||||
? matches.find(match => {
|
||||
const varMatch = variableRegexExec(match);
|
||||
return varMatch !== null && varMatch.indexOf(variableName) > -1;
|
||||
})
|
||||
: false;
|
||||
|
||||
for (let i = 1; i < args.length - 1; i++) {
|
||||
str += ' ' + args[i] || '';
|
||||
}
|
||||
|
||||
variableName = kbn.regexEscape(variableName);
|
||||
const findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
|
||||
const match = findVarRegex.exec(str);
|
||||
return match !== null;
|
||||
return !!isMatchingVariable;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,13 @@ export default class CloudWatchDatasource {
|
||||
|
||||
// valid ExtendedStatistics is like p90.00, check the pattern
|
||||
const hasInvalidStatistics = item.statistics.some(s => {
|
||||
return s.indexOf('p') === 0 && !/p\d{2}\.\d{2}/.test(s);
|
||||
if (s.indexOf('p') === 0) {
|
||||
const matches = /^p\d{2}(?:\.\d{1,2})?$/.exec(s);
|
||||
|
||||
return !matches || matches[0] !== s;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
if (hasInvalidStatistics) {
|
||||
throw { message: 'Invalid extended statistics' };
|
||||
@@ -131,7 +137,11 @@ export default class CloudWatchDatasource {
|
||||
if (res.results) {
|
||||
_.forEach(res.results, queryRes => {
|
||||
_.forEach(queryRes.series, series => {
|
||||
data.push({ target: series.name, datapoints: series.points, unit: queryRes.meta.unit || 'none' });
|
||||
const s = { target: series.name, datapoints: series.points } as any;
|
||||
if (queryRes.meta.unit) {
|
||||
s.unit = queryRes.meta.unit;
|
||||
}
|
||||
data.push(s);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ describe('CloudWatchDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel query for invalid extended statistics', () => {
|
||||
it.each(['pNN.NN', 'p9', 'p99.', 'p99.999'])('should cancel query for invalid extended statistics (%s)', stat => {
|
||||
const query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
@@ -134,7 +134,7 @@ describe('CloudWatchDatasource', () => {
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678',
|
||||
},
|
||||
statistics: ['pNN.NN'],
|
||||
statistics: [stat],
|
||||
period: '60s',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -20,7 +20,7 @@ export class PostgresDatasource {
|
||||
this.interval = (instanceSettings.jsonData || {}).timeInterval;
|
||||
}
|
||||
|
||||
interpolateVariable(value, variable) {
|
||||
interpolateVariable = (value, variable) => {
|
||||
if (typeof value === 'string') {
|
||||
if (variable.multi || variable.includeAll) {
|
||||
return this.queryModel.quoteLiteral(value);
|
||||
@@ -37,7 +37,7 @@ export class PostgresDatasource {
|
||||
return this.queryModel.quoteLiteral(v);
|
||||
});
|
||||
return quotedValues.join(',');
|
||||
}
|
||||
};
|
||||
|
||||
query(options) {
|
||||
const queries = _.filter(options.targets, target => {
|
||||
|
||||
@@ -89,7 +89,7 @@ export default class StackdriverDatasource {
|
||||
}
|
||||
|
||||
resolvePanelUnitFromTargets(targets: any[]) {
|
||||
let unit = 'none';
|
||||
let unit;
|
||||
if (targets.length > 0 && targets.every(t => t.unit === targets[0].unit)) {
|
||||
if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit)) {
|
||||
unit = stackdriverUnitMappings[targets[0].unit];
|
||||
@@ -106,21 +106,24 @@ export default class StackdriverDatasource {
|
||||
if (!queryRes.series) {
|
||||
return;
|
||||
}
|
||||
|
||||
const unit = this.resolvePanelUnitFromTargets(options.targets);
|
||||
queryRes.series.forEach(series => {
|
||||
result.push({
|
||||
let timeSerie: any = {
|
||||
target: series.name,
|
||||
datapoints: series.points,
|
||||
refId: queryRes.refId,
|
||||
meta: queryRes.meta,
|
||||
unit,
|
||||
});
|
||||
};
|
||||
if (unit) {
|
||||
timeSerie = { ...timeSerie, unit };
|
||||
}
|
||||
result.push(timeSerie);
|
||||
});
|
||||
});
|
||||
return { data: result };
|
||||
} else {
|
||||
return { data: [] };
|
||||
}
|
||||
|
||||
return { data: result };
|
||||
}
|
||||
|
||||
async annotationQuery(options) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export class FilterSegments {
|
||||
this.removeSegment.value = DefaultRemoveFilterValue;
|
||||
return Promise.resolve([this.removeSegment]);
|
||||
} else {
|
||||
return this.getFilterKeysFunc();
|
||||
return this.getFilterKeysFunc(segment, DefaultRemoveFilterValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword width-9">Group By</span>
|
||||
<div class="gf-form" ng-repeat="segment in ctrl.groupBySegments">
|
||||
<metric-segment segment="segment" get-options="ctrl.getGroupBys(segment, $index)" on-change="ctrl.groupByChanged(segment, $index)"></metric-segment>
|
||||
<metric-segment segment="segment" get-options="ctrl.getGroupBys(segment)" on-change="ctrl.groupByChanged(segment, $index)"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--grow">
|
||||
|
||||
@@ -101,6 +101,5 @@ export class StackdriverQueryCtrl extends QueryCtrl {
|
||||
this.lastQueryError = jsonBody.error.message;
|
||||
}
|
||||
}
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments';
|
||||
import { FilterSegments } from './filter_segments';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class StackdriverFilter {
|
||||
@@ -26,8 +26,10 @@ export class StackdriverFilter {
|
||||
export class StackdriverFilterCtrl {
|
||||
metricLabels: { [key: string]: string[] };
|
||||
resourceLabels: { [key: string]: string[] };
|
||||
resourceTypes: string[];
|
||||
|
||||
defaultRemoveGroupByValue = '-- remove group by --';
|
||||
resourceTypeValue = 'resource.type';
|
||||
loadLabelsPromise: Promise<any>;
|
||||
|
||||
service: string;
|
||||
@@ -72,7 +74,7 @@ export class StackdriverFilterCtrl {
|
||||
this.filterSegments = new FilterSegments(
|
||||
this.uiSegmentSrv,
|
||||
this.target,
|
||||
this.getGroupBys.bind(this, null, null, DefaultRemoveFilterValue, false),
|
||||
this.getFilterKeys.bind(this),
|
||||
this.getFilterValues.bind(this)
|
||||
);
|
||||
this.filterSegments.buildSegmentModel();
|
||||
@@ -141,6 +143,7 @@ export class StackdriverFilterCtrl {
|
||||
const data = await this.datasource.getLabels(this.target.metricType, this.target.refId);
|
||||
this.metricLabels = data.results[this.target.refId].meta.metricLabels;
|
||||
this.resourceLabels = data.results[this.target.refId].meta.resourceLabels;
|
||||
this.resourceTypes = data.results[this.target.refId].meta.resourceTypes;
|
||||
resolve();
|
||||
} catch (error) {
|
||||
if (error.data && error.data.message) {
|
||||
@@ -181,45 +184,66 @@ export class StackdriverFilterCtrl {
|
||||
this.$rootScope.$broadcast('metricTypeChanged');
|
||||
}
|
||||
|
||||
async getGroupBys(segment, index, removeText?: string, removeUsed = true) {
|
||||
async createLabelKeyElements() {
|
||||
await this.loadLabelsPromise;
|
||||
|
||||
const metricLabels = Object.keys(this.metricLabels || {})
|
||||
.filter(ml => {
|
||||
if (!removeUsed) {
|
||||
return true;
|
||||
}
|
||||
return this.target.aggregation.groupBys.indexOf('metric.label.' + ml) === -1;
|
||||
})
|
||||
.map(l => {
|
||||
return this.uiSegmentSrv.newSegment({
|
||||
value: `metric.label.${l}`,
|
||||
expandable: false,
|
||||
});
|
||||
let elements = Object.keys(this.metricLabels || {}).map(l => {
|
||||
return this.uiSegmentSrv.newSegment({
|
||||
value: `metric.label.${l}`,
|
||||
expandable: false,
|
||||
});
|
||||
});
|
||||
|
||||
const resourceLabels = Object.keys(this.resourceLabels || {})
|
||||
.filter(ml => {
|
||||
if (!removeUsed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.target.aggregation.groupBys.indexOf('resource.label.' + ml) === -1;
|
||||
})
|
||||
.map(l => {
|
||||
elements = [
|
||||
...elements,
|
||||
...Object.keys(this.resourceLabels || {}).map(l => {
|
||||
return this.uiSegmentSrv.newSegment({
|
||||
value: `resource.label.${l}`,
|
||||
expandable: false,
|
||||
});
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
|
||||
if (noValueOrPlusButton && metricLabels.length === 0 && resourceLabels.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
if (this.resourceTypes && this.resourceTypes.length > 0) {
|
||||
elements = [
|
||||
...elements,
|
||||
this.uiSegmentSrv.newSegment({
|
||||
value: this.resourceTypeValue,
|
||||
expandable: false,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
this.removeSegment.value = removeText || this.defaultRemoveGroupByValue;
|
||||
return Promise.resolve([...metricLabels, ...resourceLabels, this.removeSegment]);
|
||||
return elements;
|
||||
}
|
||||
|
||||
async getFilterKeys(segment, removeText?: string) {
|
||||
let elements = await this.createLabelKeyElements();
|
||||
|
||||
if (this.target.filters.indexOf(this.resourceTypeValue) !== -1) {
|
||||
elements = elements.filter(e => e.value !== this.resourceTypeValue);
|
||||
}
|
||||
|
||||
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
|
||||
if (noValueOrPlusButton && elements.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.removeSegment.value = removeText;
|
||||
return [...elements, this.removeSegment];
|
||||
}
|
||||
|
||||
async getGroupBys(segment) {
|
||||
let elements = await this.createLabelKeyElements();
|
||||
|
||||
elements = elements.filter(e => this.target.aggregation.groupBys.indexOf(e.value) === -1);
|
||||
const noValueOrPlusButton = !segment || segment.type === 'plus-button';
|
||||
if (noValueOrPlusButton && elements.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
this.removeSegment.value = this.defaultRemoveGroupByValue;
|
||||
return [...elements, this.removeSegment];
|
||||
}
|
||||
|
||||
groupByChanged(segment, index) {
|
||||
@@ -263,6 +287,10 @@ export class StackdriverFilterCtrl {
|
||||
return this.resourceLabels[shortKey];
|
||||
}
|
||||
|
||||
if (filterKey === this.resourceTypeValue) {
|
||||
return this.resourceTypes;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -235,8 +235,8 @@ describe('StackdriverDataSource', () => {
|
||||
beforeEach(() => {
|
||||
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]);
|
||||
});
|
||||
it('should return none', () => {
|
||||
expect(res).toEqual('none');
|
||||
it('should return undefined', () => {
|
||||
expect(res).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('and the stackdriver unit has a corresponding grafana unit', () => {
|
||||
@@ -262,16 +262,16 @@ describe('StackdriverDataSource', () => {
|
||||
beforeEach(() => {
|
||||
res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]);
|
||||
});
|
||||
it('should return the default value - none', () => {
|
||||
expect(res).toEqual('none');
|
||||
it('should return the default value of undefined', () => {
|
||||
expect(res).toBeUndefined();
|
||||
});
|
||||
});
|
||||
describe('and all target units are not the same', () => {
|
||||
beforeEach(() => {
|
||||
res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]);
|
||||
});
|
||||
it('should return the default value - none', () => {
|
||||
expect(res).toEqual('none');
|
||||
it('should return the default value of undefined', () => {
|
||||
expect(res).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user