mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'develop' into 9879-login
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export interface IProps {
|
||||
model: any;
|
||||
@@ -17,7 +17,7 @@ class EmptyListCTA extends Component<IProps, any> {
|
||||
proTipTarget
|
||||
} = this.props.model;
|
||||
return (
|
||||
<div className="empty-list-cta p-t-2 p-b-1">
|
||||
<div className="empty-list-cta">
|
||||
<div className="empty-list-cta__title">{title}</div>
|
||||
<a href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-success"><i className={buttonIcon} />{buttonTitle}</a>
|
||||
<div className="empty-list-cta__pro-tip">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`CollorPalette renders correctly 1`] = `
|
||||
<div
|
||||
className="empty-list-cta p-t-2 p-b-1"
|
||||
className="empty-list-cta"
|
||||
>
|
||||
<div
|
||||
className="empty-list-cta__title"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<input type="text" class="gf-form-input max-width-30" placeholder="Find Dashboard by name" tabindex="1" give-focus="true" ng-model="ctrl.query.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.onQueryChange()" />
|
||||
</div>
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a class="btn btn-success" href="/dashboard/new">
|
||||
<a class="btn btn-success" href="/dashboard/new?folderId={{ctrl.folderId}}">
|
||||
<i class="fa fa-plus"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
@@ -88,11 +88,11 @@
|
||||
<empty-list-cta model="{
|
||||
title: 'This folder doesn\'t have any dashboards yet',
|
||||
buttonIcon: 'gicon gicon-dashboard-new',
|
||||
buttonLink: '/dashboard/new',
|
||||
buttonLink: '/dashboard/new?folderId={{ctrl.folderId}}',
|
||||
buttonTitle: 'Create Dashboard',
|
||||
proTip: 'You can bulk move dashboards into this folder from the main dashboard list.',
|
||||
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
|
||||
proTipLinkTitle: 'Learn more',
|
||||
proTipTarget: '_blank'
|
||||
}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,15 @@
|
||||
</div>
|
||||
|
||||
<div class="search-filter-box">
|
||||
<a class="search-button-row-explore-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
|
||||
<a href="dashboard/new" class="search-filter-box-link">
|
||||
<i class="gicon gicon-dashboard-new"></i>
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="dashboards/folder/new" class="search-filter-box-link">
|
||||
<i class="gicon gicon-folder-new"></i>
|
||||
Folder
|
||||
</a>
|
||||
<a class="search-filter-box-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
|
||||
<img src="public/img/icn-dashboard-tiny.svg" width="20" /> Find dashboards on Grafana.com
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
<div ng-show="ctrl.editable && section.id > 0" ng-click="ctrl.navigateToFolder(section, $event)">
|
||||
<i class="fa fa-cog search-section__header__toggle"></i>
|
||||
</div>
|
||||
<i class="fa fa-minus search-section__header__toggle" ng-show="section.expanded"></i>
|
||||
<i class="fa fa-plus search-section__header__toggle" ng-hide="section.expanded"></i>
|
||||
<i class="fa fa-angle-down search-section__header__toggle" ng-show="section.expanded"></i>
|
||||
<i class="fa fa-angle-right search-section__header__toggle" ng-hide="section.expanded"></i>
|
||||
</a>
|
||||
|
||||
<div class="search-section__header" ng-show="section.hideHeader"></div>
|
||||
|
||||
<div ng-if="section.expanded">
|
||||
@@ -44,4 +45,5 @@
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ const template = `
|
||||
</gf-form-dropdown>
|
||||
</div>
|
||||
`;
|
||||
export class UserGroupPickerCtrl {
|
||||
export class TeamPickerCtrl {
|
||||
group: any;
|
||||
userGroupPicked: any;
|
||||
teamPicked: any;
|
||||
debouncedSearchGroups: any;
|
||||
|
||||
/** @ngInject */
|
||||
@@ -26,34 +26,34 @@ export class UserGroupPickerCtrl {
|
||||
}
|
||||
|
||||
searchGroups(query: string) {
|
||||
return Promise.resolve(this.backendSrv.get('/api/user-groups/search?perpage=10&page=1&query=' + query).then(result => {
|
||||
return _.map(result.userGroups, ug => {
|
||||
return Promise.resolve(this.backendSrv.get('/api/teams/search?perpage=10&page=1&query=' + query).then(result => {
|
||||
return _.map(result.teams, ug => {
|
||||
return {text: ug.name, value: ug};
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
onChange(option) {
|
||||
this.userGroupPicked({$group: option.value});
|
||||
this.teamPicked({$group: option.value});
|
||||
}
|
||||
}
|
||||
|
||||
export function userGroupPicker() {
|
||||
export function teamPicker() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
controller: UserGroupPickerCtrl,
|
||||
controller: TeamPickerCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
userGroupPicked: '&',
|
||||
teamPicked: '&',
|
||||
},
|
||||
link: function(scope, elem, attrs, ctrl) {
|
||||
scope.$on("user-group-picker-reset", () => {
|
||||
scope.$on("team-picker-reset", () => {
|
||||
ctrl.reset();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('userGroupPicker', userGroupPicker);
|
||||
coreModule.directive('teamPicker', teamPicker);
|
||||
@@ -47,7 +47,7 @@ import {helpModal} from './components/help/help';
|
||||
import {JsonExplorer} from './components/json_explorer/json_explorer';
|
||||
import {NavModelSrv, NavModel} from './nav_model_srv';
|
||||
import {userPicker} from './components/user_picker';
|
||||
import {userGroupPicker} from './components/user_group_picker';
|
||||
import {teamPicker} from './components/team_picker';
|
||||
import {geminiScrollbar} from './components/scroll/scroll';
|
||||
import {gfPageDirective} from './components/gf_page';
|
||||
import {orgSwitcher} from './components/org_switcher';
|
||||
@@ -85,7 +85,7 @@ export {
|
||||
NavModelSrv,
|
||||
NavModel,
|
||||
userPicker,
|
||||
userGroupPicker,
|
||||
teamPicker,
|
||||
geminiScrollbar,
|
||||
gfPageDirective,
|
||||
orgSwitcher,
|
||||
|
||||
@@ -18,22 +18,21 @@ function (_, $, coreModule) {
|
||||
elem.toggleClass('panel-in-fullscreen', false);
|
||||
});
|
||||
|
||||
var lastHideControlsVal;
|
||||
$scope.$watch('ctrl.dashboard.hideControls', function() {
|
||||
if (!$scope.dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hideControls = $scope.dashboard.hideControls;
|
||||
if (lastHideControlsVal !== hideControls) {
|
||||
elem.toggleClass('hide-controls', hideControls);
|
||||
lastHideControlsVal = hideControls;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('ctrl.playlistSrv.isPlaying', function(newValue) {
|
||||
elem.toggleClass('playlist-active', newValue === true);
|
||||
});
|
||||
|
||||
$scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
|
||||
if (newValue) {
|
||||
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
|
||||
setTimeout(function() {
|
||||
elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
|
||||
}, 10);
|
||||
} else {
|
||||
elem.removeClass('dashboard-page--settings-opening');
|
||||
elem.removeClass('dashboard-page--settings-open');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -13,7 +13,6 @@ function ($, angular, coreModule, _) {
|
||||
'templating': { src: 'public/app/features/templating/partials/editor.html'},
|
||||
'history': { html: '<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>'},
|
||||
'timepicker': { src: 'public/app/features/dashboard/timepicker/dropdown.html' },
|
||||
'add-panel': { html: '<add-panel></add-panel>' },
|
||||
'import': { html: '<dash-import dismiss="dismiss()"></dash-import>', isModal: true },
|
||||
'permissions': { html: '<dash-acl-modal dismiss="dismiss()"></dash-acl-modal>', isModal: true },
|
||||
'new-folder': {
|
||||
|
||||
@@ -31,7 +31,7 @@ export class LoadDashboardCtrl {
|
||||
export class NewDashboardCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope) {
|
||||
constructor($scope, $routeParams) {
|
||||
$scope.initDashboard({
|
||||
meta: { canStar: false, canShare: false, isNew: true },
|
||||
dashboard: {
|
||||
@@ -42,7 +42,8 @@ export class NewDashboardCtrl {
|
||||
gridPos: {x: 0, y: 0, w: 12, h: 9},
|
||||
title: 'Panel Title',
|
||||
}
|
||||
]
|
||||
],
|
||||
folderId: Number($routeParams.folderId)
|
||||
},
|
||||
}, $scope);
|
||||
}
|
||||
|
||||
@@ -104,20 +104,25 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/org/users/new', {
|
||||
templateUrl: 'public/app/features/org/partials/invite.html',
|
||||
controller : 'UserInviteCtrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/org/apikeys', {
|
||||
templateUrl: 'public/app/features/org/partials/orgApiKeys.html',
|
||||
controller : 'OrgApiKeysCtrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/org/user-groups', {
|
||||
templateUrl: 'public/app/features/org/partials/user_groups.html',
|
||||
controller : 'UserGroupsCtrl',
|
||||
.when('/org/teams', {
|
||||
templateUrl: 'public/app/features/org/partials/teams.html',
|
||||
controller : 'TeamsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
.when('/org/user-groups/edit/:id', {
|
||||
templateUrl: 'public/app/features/org/partials/user_group_details.html',
|
||||
controller : 'UserGroupDetailsCtrl',
|
||||
.when('/org/teams/edit/:id', {
|
||||
templateUrl: 'public/app/features/org/partials/team_details.html',
|
||||
controller : 'TeamDetailsCtrl',
|
||||
controllerAs: 'ctrl',
|
||||
resolve: loadOrgBundle,
|
||||
})
|
||||
|
||||
@@ -72,8 +72,8 @@ export class KeybindingSrv {
|
||||
}, 'keydown');
|
||||
}
|
||||
|
||||
showDashEditView(view) {
|
||||
var search = _.extend(this.$location.search(), {editview: view});
|
||||
showDashEditView() {
|
||||
var search = _.extend(this.$location.search(), {editview: 'settings'});
|
||||
this.$location.search(search);
|
||||
}
|
||||
|
||||
@@ -84,10 +84,6 @@ export class KeybindingSrv {
|
||||
scope.broadcastRefresh();
|
||||
});
|
||||
|
||||
this.bind('mod+h', () => {
|
||||
dashboard.hideControls = !dashboard.hideControls;
|
||||
});
|
||||
|
||||
this.bind('mod+s', e => {
|
||||
scope.appEvent('save-dashboard');
|
||||
});
|
||||
@@ -197,7 +193,7 @@ export class KeybindingSrv {
|
||||
});
|
||||
|
||||
this.bind('d s', () => {
|
||||
this.showDashEditView('settings');
|
||||
this.showDashEditView();
|
||||
});
|
||||
|
||||
this.bind('d k', () => {
|
||||
@@ -215,8 +211,14 @@ export class KeybindingSrv {
|
||||
}
|
||||
|
||||
scope.appEvent('hide-modal');
|
||||
scope.appEvent('hide-dash-editor');
|
||||
scope.appEvent('panel-change-view', {fullscreen: false, edit: false});
|
||||
|
||||
// close settings view
|
||||
var search = this.$location.search();
|
||||
if (search.editview) {
|
||||
delete search.editview;
|
||||
this.$location.search(search);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,13 @@ function popoverSrv($compile, $rootScope, $timeout) {
|
||||
openDrop = drop;
|
||||
openDrop.open();
|
||||
}, 100);
|
||||
|
||||
// return close function
|
||||
return function() {
|
||||
if (drop) {
|
||||
drop.close();
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module';
|
||||
import alertDef from '../alerting/alert_def';
|
||||
|
||||
/** @ngInject **/
|
||||
export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, popoverSrv, $compile) {
|
||||
export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, $compile) {
|
||||
|
||||
function sanitizeString(str) {
|
||||
try {
|
||||
|
||||
@@ -26,7 +26,7 @@ export class AnnotationsEditorCtrl {
|
||||
];
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private datasourceSrv) {
|
||||
constructor($scope, private datasourceSrv) {
|
||||
$scope.ctrl = this;
|
||||
|
||||
this.mode = 'list';
|
||||
@@ -62,7 +62,6 @@ export class AnnotationsEditorCtrl {
|
||||
update() {
|
||||
this.reset();
|
||||
this.mode = 'list';
|
||||
this.$scope.broadcastRefresh();
|
||||
}
|
||||
|
||||
setupNew() {
|
||||
@@ -70,32 +69,24 @@ export class AnnotationsEditorCtrl {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
backToList() {
|
||||
this.mode = 'list';
|
||||
}
|
||||
|
||||
add() {
|
||||
this.annotations.push(this.currentAnnotation);
|
||||
this.reset();
|
||||
this.mode = 'list';
|
||||
this.$scope.broadcastRefresh();
|
||||
this.$scope.dashboard.updateSubmenuVisibility();
|
||||
}
|
||||
|
||||
removeAnnotation(annotation) {
|
||||
var index = _.indexOf(this.annotations, annotation);
|
||||
this.annotations.splice(index, 1);
|
||||
this.$scope.dashboard.updateSubmenuVisibility();
|
||||
this.$scope.broadcastRefresh();
|
||||
}
|
||||
|
||||
onColorChange(newColor) {
|
||||
this.currentAnnotation.iconColor = newColor;
|
||||
}
|
||||
|
||||
annotationEnabledChange() {
|
||||
this.$scope.broadcastRefresh();
|
||||
}
|
||||
|
||||
annotationHiddenChanged() {
|
||||
this.$scope.dashboard.updateSubmenuVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('AnnotationsEditorCtrl', AnnotationsEditorCtrl);
|
||||
|
||||
@@ -1,145 +1,113 @@
|
||||
|
||||
<div ng-controller="AnnotationsEditorCtrl">
|
||||
<div class="tabbed-view-header">
|
||||
<h2 class="tabbed-view-title">
|
||||
Annotations
|
||||
</h2>
|
||||
<h3 class="dashboard-settings__header">
|
||||
<a ng-click="ctrl.backToList()">Annotations</a>
|
||||
<span ng-show="ctrl.mode === 'new'">> New</span>
|
||||
<span ng-show="ctrl.mode === 'edit'">> Edit</span>
|
||||
</h3>
|
||||
|
||||
<ul class="gf-tabs">
|
||||
<li class="gf-tabs-item" >
|
||||
<a class="gf-tabs-link" ng-click="ctrl.mode = 'list';" ng-class="{active: ctrl.mode === 'list'}">
|
||||
Queries
|
||||
</a>
|
||||
</li>
|
||||
<li class="gf-tabs-item" ng-show="ctrl.mode === 'edit'">
|
||||
<a class="gf-tabs-link" ng-class="{active: ctrl.mode === 'edit'}">
|
||||
Edit Query
|
||||
</a>
|
||||
</li>
|
||||
<li class="gf-tabs-item" ng-show="ctrl.mode === 'new'">
|
||||
<span class="active gf-tabs-link">New Query</span>
|
||||
</li>
|
||||
|
||||
<li class="gf-tabs-item" >
|
||||
<a class="gf-tabs-link" ng-click="ctrl.mode = 'help';" ng-class="{active: ctrl.mode === 'help'}">
|
||||
Help
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="tabbed-view-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tabbed-view-body">
|
||||
|
||||
<div ng-show="ctrl.mode === 'help'">
|
||||
<div class="grafana-info-box col-lg-8">
|
||||
<h5>What are Annotations?</h5>
|
||||
<p>
|
||||
Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons
|
||||
on all graph panels. When you hover over an annotation icon you can get title, tags, and text information for the event.
|
||||
In the <i>Queries</i> tab you can add queries that return annotation events.
|
||||
</p>
|
||||
<p>
|
||||
You can add annotations directly from grafana by holding CTRL or CMD + click on graph (or drag region). These will be stored in Grafana's annotation database.
|
||||
</p>
|
||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/annotations/">Annotations documentation</a> for more information.
|
||||
</div>
|
||||
<div ng-if="ctrl.mode === 'list'">
|
||||
<div class="page-action-bar" ng-if="ctrl.annotations.length > 1">
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a type="button" class="btn btn-success" ng-click="ctrl.setupNew();"><i class="fa fa-plus" ></i> New</a>
|
||||
</div>
|
||||
|
||||
<div class="editor-row row" ng-if="ctrl.mode === 'list'">
|
||||
<div ng-if="ctrl.annotations.length === 0">
|
||||
<em>No annotation queries defined</em>
|
||||
</div>
|
||||
<table class="grafana-options-table">
|
||||
<table class="filter-table filter-table--hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Query name</th>
|
||||
<th>Data source</th>
|
||||
<th colspan="3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="annotation in ctrl.annotations">
|
||||
<td style="width:90%" ng-hide="annotation.builtIn">
|
||||
<td style="width:90%" ng-hide="annotation.builtIn" class="pointer" ng-click="ctrl.edit(annotation)">
|
||||
<i class="fa fa-comment" style="color:{{annotation.iconColor}}"></i>
|
||||
{{annotation.name}}
|
||||
</td>
|
||||
<td style="width:90%" ng-show="annotation.builtIn">
|
||||
<td style="width:90%" ng-show="annotation.builtIn" class="pointer" ng-click="ctrl.edit(annotation)">
|
||||
<i class="fa fa-comment"></i>
|
||||
<em class="muted">{{annotation.name}} (Built-in)</em>
|
||||
</td>
|
||||
<td class="pointer" ng-click="ctrl.edit(annotation)">
|
||||
{{annotation.datasource || 'Default'}}
|
||||
</td>
|
||||
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
|
||||
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="ctrl.edit(annotation)" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="ctrl.removeAnnotation(annotation)" class="btn btn-danger btn-mini" ng-hide="annotation.builtIn">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.mode === 'list'">
|
||||
<div class="gf-form-button-row">
|
||||
<a type="button" class="btn gf-form-button btn-success" ng-click="ctrl.setupNew()"><i class="fa fa-plus" ></i> New</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
|
||||
<div>
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">General</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Data source</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.currentAnnotation.datasource" ng-options="f.name as f.name for f in ctrl.datasources" ng-change="ctrl.datasourceChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Enabled"
|
||||
checked="ctrl.currentAnnotation.enable"
|
||||
on-change="ctrl.annotationEnabledChange()"
|
||||
label-class="width-7">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Hidden"
|
||||
tooltip="Hides the annotation query toggle from showing at the top of the dashboard"
|
||||
checked="ctrl.currentAnnotation.hide"
|
||||
on-change="ctrl.annotationHiddenChanged()"
|
||||
label-class="width-7">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Color</label>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="ctrl.currentAnnotation.iconColor" onChange="ctrl.onColorChange"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="section-heading">Query</h5>
|
||||
<rebuild-on-change property="ctrl.currentDatasource">
|
||||
<plugin-component type="annotations-query-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</button>
|
||||
<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="ctrl.update()">Update</button>
|
||||
<!-- empty list cta, there is always one built in query -->
|
||||
<div ng-if="ctrl.annotations.length === 1" class="p-t-2">
|
||||
<div class="empty-list-cta">
|
||||
<div class="empty-list-cta__title">There are no custom annotation queries added yet</div>
|
||||
<a ng-click="ctrl.setupNew()" class="empty-list-cta__button btn btn-xlarge btn-success">
|
||||
<i class="gicon gicon-dashboard-new"></i>
|
||||
Add Annotation Query
|
||||
</a>
|
||||
<div class="grafana-info-box">
|
||||
<h5>What are Annotations?</h5>
|
||||
<p>
|
||||
Annotations provide a way to integrate event data into your graphs. They are visualized as vertical lines and icons
|
||||
on all graph panels. When you hover over an annotation icon you can get event text & tags for the event. You can add annotation events
|
||||
directly from grafana by holding CTRL or CMD + click on graph (or drag region). These will be stored in Grafana's annotation database.
|
||||
</p>
|
||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/annotations/">Annotations documentation</a> for more information.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">General</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Data source</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.currentAnnotation.datasource" ng-options="f.name as f.name for f in ctrl.datasources" ng-change="ctrl.datasourceChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label="Enabled" checked="ctrl.currentAnnotation.enable" label-class="width-7">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Hidden" tooltip="Hides the annotation query toggle from showing at the top of the dashboard" checked="ctrl.currentAnnotation.hide" label-class="width-7">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Color</label>
|
||||
<span class="gf-form-label">
|
||||
<color-picker color="ctrl.currentAnnotation.iconColor" onChange="ctrl.onColorChange"></color-picker>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="section-heading">Query</h5>
|
||||
<rebuild-on-change property="ctrl.currentDatasource">
|
||||
<plugin-component type="annotations-query-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</button>
|
||||
<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="ctrl.update()">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.newType === 'Group'">
|
||||
<user-group-picker user-group-picked="ctrl.groupPicked($group)"></user-group-picker>
|
||||
<team-picker team-picked="ctrl.groupPicked($group)"></team-picker>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -101,9 +101,9 @@
|
||||
<!-- </a> -->
|
||||
<!-- </td> -->
|
||||
<!-- </tr> -->
|
||||
<!-- <tr ng-repeat="permission in ctrl.userGroupPermissions" class="permissionlist__item"> -->
|
||||
<!-- <tr ng-repeat="permission in ctrl.teamPermissions" class="permissionlist__item"> -->
|
||||
<!-- <td><i class="fa fa-fw fa-users"></i></td> -->
|
||||
<!-- <td>{{permission.userGroup}}</td> -->
|
||||
<!-- <td>{{permission.team}}</td> -->
|
||||
<!-- <td><select class="gf-form-input gf-size-auto" ng-model="permission.permissions" ng-options="p.value as p.text for p in ctrl.permissionTypeOptions" ng-change="ctrl.updatePermission(permission)"></select></td> -->
|
||||
<!-- <td class="text-right"> -->
|
||||
<!-- <a ng-click="ctrl.removePermission(permission)" class="btn btn-danger btn-small"> -->
|
||||
|
||||
@@ -12,7 +12,7 @@ export class AclCtrl {
|
||||
{value: 4, text: 'Admin'}
|
||||
];
|
||||
aclTypes = [
|
||||
{value: 'Group', text: 'User Group'},
|
||||
{value: 'Group', text: 'Team'},
|
||||
{value: 'User', text: 'User'},
|
||||
{value: 'Viewer', text: 'Everyone With Viewer Role'},
|
||||
{value: 'Editor', text: 'Everyone With Editor Role'}
|
||||
@@ -58,10 +58,10 @@ export class AclCtrl {
|
||||
item.nameHtml = this.$sce.trustAsHtml(item.userLogin);
|
||||
item.sortName = item.userLogin;
|
||||
item.sortRank = 10;
|
||||
} else if (item.userGroupId > 0) {
|
||||
} else if (item.teamId > 0) {
|
||||
item.icon = "fa fa-fw fa-users";
|
||||
item.nameHtml = this.$sce.trustAsHtml(item.userGroup);
|
||||
item.sortName = item.userGroup;
|
||||
item.nameHtml = this.$sce.trustAsHtml(item.team);
|
||||
item.sortName = item.team;
|
||||
item.sortRank = 20;
|
||||
} else if (item.role) {
|
||||
item.icon = "fa fa-fw fa-street-view";
|
||||
@@ -89,7 +89,7 @@ export class AclCtrl {
|
||||
updated.push({
|
||||
id: item.id,
|
||||
userId: item.userId,
|
||||
userGroupId: item.userGroupId,
|
||||
teamId: item.teamId,
|
||||
role: item.role,
|
||||
permission: item.permission,
|
||||
});
|
||||
@@ -144,7 +144,7 @@ export class AclCtrl {
|
||||
|
||||
return (origItem.role && newItem.role && origItem.role === newItem.role) ||
|
||||
(origItem.userId && newItem.userId && origItem.userId === newItem.userId) ||
|
||||
(origItem.userGroupId && newItem.userGroupId && origItem.userGroupId === newItem.userGroupId);
|
||||
(origItem.teamId && newItem.teamId && origItem.teamId === newItem.teamId);
|
||||
}
|
||||
|
||||
userPicked(user) {
|
||||
@@ -153,8 +153,8 @@ export class AclCtrl {
|
||||
}
|
||||
|
||||
groupPicked(group) {
|
||||
this.addNewItem({userGroupId: group.id, userGroup: group.name, permission: 1});
|
||||
this.$scope.$broadcast('user-group-picker-reset');
|
||||
this.addNewItem({teamId: group.id, team: group.name, permission: 1});
|
||||
this.$scope.$broadcast('team-picker-reset');
|
||||
}
|
||||
|
||||
removeItem(index) {
|
||||
@@ -179,7 +179,7 @@ export function dashAclModal() {
|
||||
export interface FormModel {
|
||||
dashboardId: number;
|
||||
userId?: number;
|
||||
userGroupId?: number;
|
||||
teamId?: number;
|
||||
PermissionType: number;
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ export interface DashboardAcl {
|
||||
userId?: number;
|
||||
userLogin?: string;
|
||||
userEmail?: string;
|
||||
userGroupId?: number;
|
||||
userGroup?: string;
|
||||
teamId?: number;
|
||||
team?: string;
|
||||
permission?: number;
|
||||
permissionName?: string;
|
||||
role?: string;
|
||||
|
||||
@@ -40,12 +40,12 @@ describe('AclCtrl', () => {
|
||||
|
||||
ctx.ctrl.userPicked(userItem);
|
||||
|
||||
const userGroupItem = {
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: 'ug1',
|
||||
};
|
||||
|
||||
ctx.ctrl.groupPicked(userGroupItem);
|
||||
ctx.ctrl.groupPicked(teamItem);
|
||||
|
||||
ctx.ctrl.newType = 'Editor';
|
||||
ctx.ctrl.typeChanged();
|
||||
@@ -54,10 +54,10 @@ describe('AclCtrl', () => {
|
||||
ctx.ctrl.typeChanged();
|
||||
});
|
||||
|
||||
it('should sort the result by role, user group and user', () => {
|
||||
it('should sort the result by role, team and user', () => {
|
||||
expect(ctx.ctrl.items[0].role).to.eql('Viewer');
|
||||
expect(ctx.ctrl.items[1].role).to.eql('Editor');
|
||||
expect(ctx.ctrl.items[2].userGroupId).to.eql(2);
|
||||
expect(ctx.ctrl.items[2].teamId).to.eql(2);
|
||||
expect(ctx.ctrl.items[3].userId).to.eql(2);
|
||||
});
|
||||
|
||||
@@ -71,7 +71,7 @@ describe('AclCtrl', () => {
|
||||
expect(backendSrv.post.getCall(0).args[1].items[0].permission).to.eql(1);
|
||||
expect(backendSrv.post.getCall(0).args[1].items[1].role).to.eql('Editor');
|
||||
expect(backendSrv.post.getCall(0).args[1].items[1].permission).to.eql(1);
|
||||
expect(backendSrv.post.getCall(0).args[1].items[2].userGroupId).to.eql(2);
|
||||
expect(backendSrv.post.getCall(0).args[1].items[2].teamId).to.eql(2);
|
||||
expect(backendSrv.post.getCall(0).args[1].items[2].permission).to.eql(1);
|
||||
expect(backendSrv.post.getCall(0).args[1].items[3].userId).to.eql(2);
|
||||
expect(backendSrv.post.getCall(0).args[1].items[3].permission).to.eql(1);
|
||||
@@ -124,19 +124,19 @@ describe('AclCtrl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when duplicate user group permissions are added', () => {
|
||||
describe('when duplicate team permissions are added', () => {
|
||||
beforeEach(() => {
|
||||
backendSrv.get.reset();
|
||||
backendSrv.post.reset();
|
||||
ctx.ctrl.items = [];
|
||||
|
||||
const userGroupItem = {
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: 'ug1',
|
||||
};
|
||||
|
||||
ctx.ctrl.groupPicked(userGroupItem);
|
||||
ctx.ctrl.groupPicked(userGroupItem);
|
||||
ctx.ctrl.groupPicked(teamItem);
|
||||
ctx.ctrl.groupPicked(teamItem);
|
||||
});
|
||||
|
||||
it('should throw a validation error', () => {
|
||||
@@ -148,25 +148,25 @@ describe('AclCtrl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when one inherited and one not inherited user group permission are added', () => {
|
||||
describe('when one inherited and one not inherited team permission are added', () => {
|
||||
beforeEach(() => {
|
||||
backendSrv.get.reset();
|
||||
backendSrv.post.reset();
|
||||
ctx.ctrl.items = [];
|
||||
|
||||
const inheritedUserGroupItem = {
|
||||
const inheritedTeamItem = {
|
||||
id: 2,
|
||||
name: 'ug1',
|
||||
dashboardId: -1
|
||||
};
|
||||
|
||||
ctx.ctrl.items.push(inheritedUserGroupItem);
|
||||
ctx.ctrl.items.push(inheritedTeamItem);
|
||||
|
||||
const userGroupItem = {
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: 'ug1',
|
||||
};
|
||||
ctx.ctrl.groupPicked(userGroupItem);
|
||||
ctx.ctrl.groupPicked(teamItem);
|
||||
});
|
||||
|
||||
it('should not throw a validation error', () => {
|
||||
|
||||
@@ -19,15 +19,16 @@ import './export/export_modal';
|
||||
import './export_data/export_data_modal';
|
||||
import './ad_hoc_filters';
|
||||
import './repeat_option/repeat_option';
|
||||
import './dashgrid/DashboardGrid';
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
import './dashgrid/PanelLoader';
|
||||
import './dashgrid/RowOptions';
|
||||
import './acl/acl';
|
||||
import './folder_picker/picker';
|
||||
import './folder_modal/folder';
|
||||
import './move_to_folder_modal/move_to_folder';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import './settings/settings';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {DashboardListCtrl} from './dashboard_list_ctrl';
|
||||
import {FolderDashboardsCtrl} from './folder_dashboards_ctrl';
|
||||
import {FolderPermissionsCtrl} from './folder_permissions_ctrl';
|
||||
|
||||
@@ -7,7 +7,7 @@ export class CreateFolderCtrl {
|
||||
titleTouched = false;
|
||||
|
||||
constructor(private backendSrv, private $location, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('create', 'folder');
|
||||
this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0);
|
||||
}
|
||||
|
||||
create() {
|
||||
|
||||
@@ -122,12 +122,6 @@ export class DashboardCtrl implements PanelContainer {
|
||||
this.$rootScope.$broadcast("refresh");
|
||||
}
|
||||
|
||||
onFolderChange(folder) {
|
||||
this.dashboard.folderId = folder.id;
|
||||
this.dashboard.meta.folderId = folder.id;
|
||||
this.dashboard.meta.folderTitle= folder.title;
|
||||
}
|
||||
|
||||
getPanelContainer() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ export class DashboardModel {
|
||||
graphTooltip: any;
|
||||
time: any;
|
||||
timepicker: any;
|
||||
hideControls: any;
|
||||
templating: any;
|
||||
annotations: any;
|
||||
refresh: any;
|
||||
@@ -67,7 +66,6 @@ export class DashboardModel {
|
||||
this.timezone = data.timezone || '';
|
||||
this.editable = data.editable !== false;
|
||||
this.graphTooltip = data.graphTooltip || 0;
|
||||
this.hideControls = data.hideControls || false;
|
||||
this.time = data.time || {from: 'now-6h', to: 'now'};
|
||||
this.timepicker = data.timepicker || {};
|
||||
this.templating = this.ensureListExist(data.templating);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import ReactGridLayout from 'react-grid-layout';
|
||||
import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT} from 'app/core/constants';
|
||||
import {DashboardPanel} from './DashboardPanel';
|
||||
@@ -174,6 +173,3 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.directive('dashboardGrid', function(reactDirective) {
|
||||
return reactDirective(DashboardGrid, [['getPanelContainer', {watchDepth: 'reference', wrapApply: false}]]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
import { DashboardGrid } from './DashboardGrid';
|
||||
|
||||
react2AngularDirective('dashboardGrid', DashboardGrid, [['getPanelContainer', {watchDepth: 'reference', wrapApply: false}]]);
|
||||
@@ -27,40 +27,35 @@
|
||||
<i class="gicon gicon-add-panel"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn navbar-button" ng-show="::ctrl.dashboard.meta.canStar" ng-click="ctrl.starDashboard()" bs-tooltip="'Mark as favorite'" data-placement="bottom">
|
||||
<button class="btn navbar-button navbar-button--star" ng-show="::ctrl.dashboard.meta.canStar" ng-click="ctrl.starDashboard()" bs-tooltip="'Mark as favorite'" data-placement="bottom">
|
||||
<i class="fa" ng-class="{'fa-star-o': !ctrl.dashboard.meta.isStarred, 'fa-star': ctrl.dashboard.meta.isStarred}"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn navbar-button" ng-show="::ctrl.dashboard.meta.canShare" ng-click="ctrl.shareDashboard(0)" bs-tooltip="'Share dashboard'" data-placement="bottom">
|
||||
<button class="btn navbar-button navbar-button--share" ng-show="::ctrl.dashboard.meta.canShare" ng-click="ctrl.shareDashboard(0)" bs-tooltip="'Share dashboard'" data-placement="bottom">
|
||||
<i class="fa fa-share-square-o"></i></a>
|
||||
</button>
|
||||
|
||||
<button class="btn navbar-button" ng-show="::ctrl.dashboard.meta.canSave" ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom">
|
||||
<button class="btn navbar-button navbar-button--save" ng-show="::ctrl.dashboard.meta.canSave" ng-click="ctrl.saveDashboard()" bs-tooltip="'Save dashboard <br> CTRL+S'" data-placement="bottom">
|
||||
<i class="fa fa-save"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn navbar-button" ng-if="::ctrl.dashboard.snapshot.originalUrl" ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom">
|
||||
<button class="btn navbar-button navbar-button--snapshot-origin" ng-if="::ctrl.dashboard.snapshot.originalUrl" ng-href="{{ctrl.dashboard.snapshot.originalUrl}}" bs-tooltip="'Open original dashboard'" data-placement="bottom">
|
||||
<i class="fa fa-link"></i>
|
||||
</button>
|
||||
|
||||
<div class="dropdown">
|
||||
<button class="btn navbar-button" data-toggle="dropdown" bs-tooltip="'Settings'" data-placement="bottom">
|
||||
<i class="fa fa-cog"></i>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu--navbar">
|
||||
<li ng-repeat="navItem in ::ctrl.navModel.menu" ng-class="{active: navItem.active}">
|
||||
<a class="pointer" ng-href="{{::navItem.url}}" ng-click="ctrl.navItemClicked(navItem, $event)">
|
||||
<i class="{{::navItem.icon}}" ng-show="::navItem.icon"></i>
|
||||
{{::navItem.title}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="btn navbar-button navbar-button--settings" ng-click="ctrl.toggleSettings()" bs-tooltip="'Settings'" data-placement="bottom">
|
||||
<i class="fa fa-cog"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<gf-time-picker class="gf-timepicker-nav" dashboard="ctrl.dashboard" ng-if="!ctrl.dashboard.timepicker.hidden"></gf-time-picker>
|
||||
|
||||
<div class="navbar-buttons navbar-buttons--close">
|
||||
<button class="btn navbar-button navbar-button--primary" ng-click="ctrl.close()" bs-tooltip="'Back to dashboard'" data-placement="bottom">
|
||||
<i class="fa fa-reply"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<dashboard-search></dashboard-search>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import angular from 'angular';
|
||||
import {appEvents, NavModel} from 'app/core/core';
|
||||
@@ -15,13 +14,11 @@ export class DashNavCtrl {
|
||||
private $rootScope,
|
||||
private dashboardSrv,
|
||||
private $location,
|
||||
private backendSrv,
|
||||
public playlistSrv,
|
||||
navModelSrv) {
|
||||
this.navModel = navModelSrv.getDashboardNav(this.dashboard, this);
|
||||
|
||||
appEvents.on('save-dashboard', this.saveDashboard.bind(this), $scope);
|
||||
appEvents.on('delete-dashboard', this.deleteDashboard.bind(this), $scope);
|
||||
|
||||
if (this.dashboard.meta.isSnapshot) {
|
||||
var meta = this.dashboard.meta;
|
||||
@@ -32,13 +29,26 @@ export class DashNavCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
openEditView(editview) {
|
||||
var search = _.extend(this.$location.search(), {editview: editview});
|
||||
toggleSettings() {
|
||||
let search = this.$location.search();
|
||||
if (search.editview) {
|
||||
delete search.editview;
|
||||
} else {
|
||||
search.editview = 'settings';
|
||||
}
|
||||
this.$location.search(search);
|
||||
}
|
||||
|
||||
showHelpModal() {
|
||||
appEvents.emit('show-modal', {templateHtml: '<help-modal></help-modal>'});
|
||||
close() {
|
||||
let search = this.$location.search();
|
||||
if (search.editview) {
|
||||
delete search.editview;
|
||||
}
|
||||
if (search.fullscreen) {
|
||||
delete search.fullscreen;
|
||||
delete search.edit;
|
||||
}
|
||||
this.$location.search(search);
|
||||
}
|
||||
|
||||
starDashboard() {
|
||||
@@ -63,73 +73,10 @@ export class DashNavCtrl {
|
||||
angular.element(evt.currentTarget).tooltip('hide');
|
||||
}
|
||||
|
||||
makeEditable() {
|
||||
this.dashboard.editable = true;
|
||||
|
||||
return this.dashboardSrv.saveDashboard({makeEditable: true, overwrite: false}).then(() => {
|
||||
// force refresh whole page
|
||||
window.location.href = window.location.href;
|
||||
});
|
||||
}
|
||||
|
||||
exitFullscreen() {
|
||||
this.$rootScope.appEvent('panel-change-view', {fullscreen: false, edit: false});
|
||||
}
|
||||
|
||||
saveDashboard() {
|
||||
return this.dashboardSrv.saveDashboard();
|
||||
}
|
||||
|
||||
deleteDashboard() {
|
||||
var confirmText = '';
|
||||
var text2 = this.dashboard.title;
|
||||
|
||||
const alerts = _.sumBy(this.dashboard.panels, panel => {
|
||||
return panel.alert ? 1 : 0;
|
||||
});
|
||||
|
||||
if (alerts > 0) {
|
||||
confirmText = 'DELETE';
|
||||
text2 = `This dashboard contains ${alerts} alerts. Deleting this dashboard will also delete those alerts`;
|
||||
}
|
||||
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Do you want to delete this dashboard?',
|
||||
text2: text2,
|
||||
icon: 'fa-trash',
|
||||
confirmText: confirmText,
|
||||
yesText: 'Delete',
|
||||
onConfirm: () => {
|
||||
this.dashboard.meta.canSave = false;
|
||||
this.deleteDashboardConfirmed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteDashboardConfirmed() {
|
||||
this.backendSrv.delete('/api/dashboards/db/' + this.dashboard.meta.slug).then(() => {
|
||||
appEvents.emit('alert-success', ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
|
||||
this.$location.url('/');
|
||||
});
|
||||
}
|
||||
|
||||
saveDashboardAs() {
|
||||
return this.dashboardSrv.showSaveAsModal();
|
||||
}
|
||||
|
||||
viewJson() {
|
||||
var clone = this.dashboard.getSaveModelClone();
|
||||
|
||||
this.$rootScope.appEvent('show-json-editor', {
|
||||
object: clone,
|
||||
});
|
||||
}
|
||||
|
||||
onFolderChange(folderId) {
|
||||
this.dashboard.folderId = folderId;
|
||||
}
|
||||
|
||||
showSearch() {
|
||||
this.$rootScope.appEvent('show-dash-search');
|
||||
}
|
||||
|
||||
@@ -1,146 +1,118 @@
|
||||
<div class="tabbed-view-header">
|
||||
<h2 class="tabbed-view-title">
|
||||
Version history
|
||||
</h2>
|
||||
<h3 class="dashboard-settings__header">
|
||||
<a ng-click="ctrl.switchMode('list')">Versions</a>
|
||||
<span ng-show="ctrl.mode === 'compare'">
|
||||
> Comparing {{ctrl.baseInfo.version}}
|
||||
<i class="fa fa-arrows-h"></i>
|
||||
{{ctrl.newInfo.version}}
|
||||
<cite class="muted" ng-if="ctrl.isNewLatest">(Latest)</cite>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<ul class="gf-tabs">
|
||||
<li class="gf-tabs-item" >
|
||||
<a class="gf-tabs-link" ng-click="ctrl.switchMode('list');" ng-class="{active: ctrl.mode === 'list'}">
|
||||
List
|
||||
</a>
|
||||
</li>
|
||||
<li class="gf-tabs-item" ng-show="ctrl.mode === 'compare'">
|
||||
<span class="active gf-tabs-link">
|
||||
Version Comparison
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div ng-if="ctrl.mode === 'list'">
|
||||
<div ng-if="ctrl.loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<em>Fetching history list…</em>
|
||||
</div>
|
||||
|
||||
<button class="tabbed-view-close-btn" ng-click="ctrl.dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div ng-if="!ctrl.loading">
|
||||
<div class="gf-form-group">
|
||||
<table class="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="width-4"></th>
|
||||
<th class="width-4">Version</th>
|
||||
<th class="width-14">Date</th>
|
||||
<th class="width-10">Updated By</th>
|
||||
<th>Notes</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="revision in ctrl.revisions">
|
||||
<td class="filter-table__switch-cell" bs-tooltip="!revision.checked && ctrl.canCompare ? 'You can only compare 2 versions at a time' : ''" data-placement="right">
|
||||
<gf-form-switch switch-class="gf-form-switch--table-cell" checked="revision.checked" on-change="ctrl.revisionSelectionChanged()" ng-disabled="!revision.checked && ctrl.canCompare">
|
||||
</gf-form-switch>
|
||||
</td>
|
||||
<td class="text-center">{{revision.version}}</td>
|
||||
<td>{{revision.createdDateString}}</td>
|
||||
<td>{{revision.createdBy}}</td>
|
||||
<td>{{revision.message}}</td>
|
||||
<td class="text-right">
|
||||
<a class="btn btn-inverse btn-small" ng-show="revision.version !== ctrl.dashboard.version" ng-click="ctrl.restore(revision.version)">
|
||||
<i class="fa fa-history"></i> Restore
|
||||
</a>
|
||||
<a class="btn btn-outline-disabled btn-small" ng-show="revision.version === ctrl.dashboard.version">
|
||||
<i class="fa fa-check"></i> Latest
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="tabbed-view-body">
|
||||
|
||||
<div ng-if="ctrl.mode === 'list'">
|
||||
<div ng-if="ctrl.loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<em>Fetching history list…</em>
|
||||
</div>
|
||||
|
||||
<div ng-if="!ctrl.loading">
|
||||
<div class="gf-form-group">
|
||||
<table class="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="width-4"></th>
|
||||
<th class="width-4">Version</th>
|
||||
<th class="width-14">Date</th>
|
||||
<th class="width-10">Updated By</th>
|
||||
<th>Notes</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="revision in ctrl.revisions">
|
||||
<td class="filter-table__switch-cell" bs-tooltip="!revision.checked && ctrl.canCompare ? 'You can only compare 2 versions at a time' : ''" data-placement="right">
|
||||
<gf-form-switch switch-class="gf-form-switch--table-cell"
|
||||
checked="revision.checked"
|
||||
on-change="ctrl.revisionSelectionChanged()"
|
||||
ng-disabled="!revision.checked && ctrl.canCompare">
|
||||
</gf-form-switch>
|
||||
</td>
|
||||
<td class="text-center">{{revision.version}}</td>
|
||||
<td>{{revision.createdDateString}}</td>
|
||||
<td>{{revision.createdBy}}</td>
|
||||
<td>{{revision.message}}</td>
|
||||
<td class="text-right">
|
||||
<a class="btn btn-inverse btn-small" ng-show="revision.version !== ctrl.dashboard.version" ng-click="ctrl.restore(revision.version)">
|
||||
<i class="fa fa-history"></i> Restore
|
||||
</a>
|
||||
<a class="btn btn-outline-disabled btn-small" ng-show="revision.version === ctrl.dashboard.version">
|
||||
<i class="fa fa-check"></i> Latest
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div ng-if="ctrl.appending">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<em>Fetching more entries…</em>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-show="ctrl.mode === 'list'">
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button"
|
||||
class="btn gf-form-button btn-secondary"
|
||||
ng-if="ctrl.revisions.length > 1"
|
||||
ng-disabled="!ctrl.canCompare"
|
||||
ng-click="ctrl.getDiff(ctrl.diff)"
|
||||
bs-tooltip="ctrl.canCompare ? '' : 'Select 2 versions to start comparing'" data-placement="bottom">
|
||||
<i class="fa fa-code-fork" ></i> Compare versions
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn gf-form-button btn-inverse"
|
||||
ng-if="ctrl.revisions.length >= ctrl.limit"
|
||||
ng-click="ctrl.addToLog()"
|
||||
ng-disabled="ctrl.isLastPage()">
|
||||
Show more versions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-tab-with-sidemenu" ng-if="ctrl.mode === 'compare'">
|
||||
<aside class="edit-sidemenu-aside">
|
||||
<ul class="edit-sidemenu">
|
||||
<li ng-class="{active: ctrl.diff === 'basic'}"><a ng-click="ctrl.getDiff('basic')" href="">Change Summary</a></li>
|
||||
<li ng-class="{active: ctrl.diff === 'html'}"><a ng-click="ctrl.getDiff('json')" href="">JSON Diff</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<div class="edit-tab-content">
|
||||
<div ng-if="ctrl.loading">
|
||||
<div ng-if="ctrl.appending">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<em>Fetching changes…</em>
|
||||
<em>Fetching more entries…</em>
|
||||
</div>
|
||||
|
||||
<div ng-if="!ctrl.loading">
|
||||
<a type="button"
|
||||
class="btn gf-form-button btn-secondary pull-right"
|
||||
ng-click="ctrl.restore(ctrl.baseInfo.version)"
|
||||
ng-if="ctrl.isNewLatest">
|
||||
<i class="fa fa-history" ></i> Restore to version {{ctrl.baseInfo.version}}
|
||||
</a>
|
||||
<h4>
|
||||
Comparing Version {{ctrl.baseInfo.version}}
|
||||
<i class="fa fa-arrows-h"></i>
|
||||
Version {{ctrl.newInfo.version}}
|
||||
<cite class="muted" ng-if="ctrl.isNewLatest">(Latest)</cite>
|
||||
</h4>
|
||||
<section>
|
||||
<p class="small muted">
|
||||
<strong>Version {{ctrl.newInfo.version}}</strong> updated by
|
||||
<span>{{ctrl.newInfo.createdBy}} </span>
|
||||
<span>{{ctrl.newInfo.ageString}}</span>
|
||||
<span> - {{ctrl.newInfo.message}}</span>
|
||||
</p>
|
||||
<p class="small muted">
|
||||
<strong>Version {{ctrl.baseInfo.version}}</strong> updated by
|
||||
<span>{{ctrl.baseInfo.createdBy}} </span>
|
||||
<span>{{ctrl.baseInfo.ageString}}</span>
|
||||
<span> - {{ctrl.baseInfo.message}}</span>
|
||||
</p>
|
||||
</section>
|
||||
<div id="delta" diff-delta>
|
||||
<div class="delta-basic" ng-show="ctrl.diff === 'basic'" compile="ctrl.delta.basic"></div>
|
||||
<div class="delta-html" ng-show="ctrl.diff === 'json'" compile="ctrl.delta.json"></div>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button"
|
||||
class="btn gf-form-button btn-inverse"
|
||||
ng-if="ctrl.revisions.length >= ctrl.limit"
|
||||
ng-click="ctrl.addToLog()"
|
||||
ng-disabled="ctrl.isLastPage()">
|
||||
Show more versions
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-success"
|
||||
ng-if="ctrl.revisions.length > 1"
|
||||
ng-disabled="!ctrl.canCompare"
|
||||
ng-click="ctrl.getDiff(ctrl.diff)"
|
||||
bs-tooltip="ctrl.canCompare ? '' : 'Select 2 versions to start comparing'" data-placement="bottom">
|
||||
<i class="fa fa-code-fork" ></i> Compare versions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.mode === 'compare'">
|
||||
<div ng-if="ctrl.loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
<em>Fetching changes…</em>
|
||||
</div>
|
||||
|
||||
<div ng-if="!ctrl.loading">
|
||||
<button type="button"
|
||||
class="btn btn-danger pull-right"
|
||||
ng-click="ctrl.restore(ctrl.baseInfo.version)"
|
||||
ng-if="ctrl.isNewLatest">
|
||||
<i class="fa fa-history" ></i> Restore to version {{ctrl.baseInfo.version}}
|
||||
</button>
|
||||
<section>
|
||||
<p class="small muted">
|
||||
<strong>Version {{ctrl.newInfo.version}}</strong> updated by
|
||||
<span>{{ctrl.newInfo.createdBy}} </span>
|
||||
<span>{{ctrl.newInfo.ageString}}</span>
|
||||
<span> - {{ctrl.newInfo.message}}</span>
|
||||
</p>
|
||||
<p class="small muted">
|
||||
<strong>Version {{ctrl.baseInfo.version}}</strong> updated by
|
||||
<span>{{ctrl.baseInfo.createdBy}} </span>
|
||||
<span>{{ctrl.baseInfo.ageString}}</span>
|
||||
<span> - {{ctrl.baseInfo.message}}</span>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div id="delta" diff-delta>
|
||||
<div class="delta-basic" compile="ctrl.delta.basic"></div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-secondary" ng-click="ctrl.getDiff('json')">View JSON Diff</button>
|
||||
</div>
|
||||
|
||||
<div class="delta-html" ng-show="ctrl.diff === 'json'" compile="ctrl.delta.json"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import './history_srv';
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
<div class="modal-body" ng-controller="AddAnnotationModalCtrl">
|
||||
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
Add Annotation
|
||||
</h2>
|
||||
|
||||
<a class="modal-header-close" ng-click="ctrl.close()">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="share-modal-body">
|
||||
<div class="share-modal-header">
|
||||
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-tag"></i>
|
||||
</div>
|
||||
|
||||
<div class="share-modal-content">
|
||||
|
||||
<div class="gf-form-group share-modal-options">
|
||||
<p class="share-modal-info-text">
|
||||
Add annotation details.
|
||||
</p>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Title</span>
|
||||
<input type="text" ng-model="ctrl.annotation.title" class="gf-form-input max-width-20">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8" ng-if="!ctrl.annotation.timeTo">Time</span>
|
||||
<span class="gf-form-label width-8" ng-if="ctrl.annotation.timeTo">Time Start</span>
|
||||
<input type="text" ng-model="ctrl.annotation.time" class="gf-form-input max-width-20">
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.annotation.timeTo">
|
||||
<span class="gf-form-label width-8">Time Stop</span>
|
||||
<input type="text" ng-model="ctrl.annotation.timeTo" class="gf-form-input max-width-20">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>Description</h6>
|
||||
</div>
|
||||
<div class="gf-form-group share-modal-options">
|
||||
<div class="gf-form">
|
||||
<textarea rows="3" class="gf-form-input width-27" ng-model="ctrl.annotation.text"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.addAnnotation()">
|
||||
<i class="fa fa-pencil"></i>
|
||||
Add Annotation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,42 +2,42 @@
|
||||
|
||||
<div class="page-container page-body" ng-cloak>
|
||||
|
||||
<form name="ctrl.saveForm" ng-submit="ctrl.create()" class="modal-content folder-modal" novalidate>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label width-10">Folder name</label>
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-model-options="{ debounce: 400 }" ng-class="{'validation-error': ctrl.nameExists || !ctrl.dash.title}">
|
||||
<label class="gf-form-label text-success" ng-if="!ctrl.nameExists && ctrl.title">
|
||||
<i class="fa fa-check"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="page-sub-heading">New Dashboard Folder</h3>
|
||||
|
||||
<div class="gf-form-inline" ng-if="ctrl.nameExists">
|
||||
<div class="gf-form offset-width-10 gf-form--grow">
|
||||
<label class="gf-form-label text-warning gf-form-label--grow">
|
||||
<i class="fa fa-warning"></i>
|
||||
A Folder or Dashboard with the same name already exists
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<form name="ctrl.saveForm" ng-submit="ctrl.create()" novalidate>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label width-10">Folder name</label>
|
||||
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-model-options="{ debounce: 400 }" ng-class="{'validation-error': ctrl.nameExists || !ctrl.dash.title}">
|
||||
<label class="gf-form-label text-success" ng-if="!ctrl.nameExists && ctrl.title">
|
||||
<i class="fa fa-check"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-if="!ctrl.title && ctrl.titleTouched">
|
||||
<div class="gf-form offset-width-10 gf-form--grow">
|
||||
<label class="gf-form-label text-warning gf-form-label--grow">
|
||||
<i class="fa fa-warning"></i>
|
||||
A Folder should have a name
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline" ng-if="ctrl.nameExists">
|
||||
<div class="gf-form offset-width-10 gf-form--grow">
|
||||
<label class="gf-form-label text-warning gf-form-label--grow">
|
||||
<i class="fa fa-warning"></i>
|
||||
A Folder or Dashboard with the same name already exists
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success width-12" ng-disabled="ctrl.nameExists || ctrl.title.length === 0">
|
||||
<i class="fa fa-save"></i> Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="gf-form-inline" ng-if="!ctrl.title && ctrl.titleTouched">
|
||||
<div class="gf-form offset-width-10 gf-form--grow">
|
||||
<label class="gf-form-label text-warning gf-form-label--grow">
|
||||
<i class="fa fa-warning"></i>
|
||||
A Folder should have a name
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success width-12" ng-disabled="ctrl.nameExists || ctrl.title.length === 0">
|
||||
<i class="fa fa-save"></i> Create
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<div class="tabbed-view-header">
|
||||
<h2 class="tabbed-view-title">
|
||||
Settings
|
||||
</h2>
|
||||
|
||||
<ul class="gf-tabs">
|
||||
<li class="gf-tabs-item" ng-repeat="tab in ::['General', 'Links', 'Time picker']">
|
||||
<a class="gf-tabs-link" ng-click="ctrl.editTab = $index" ng-class="{active: ctrl.editTab === $index}">
|
||||
{{::tab}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button class="tabbed-view-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tabbed-view-body">
|
||||
<div ng-if="ctrl.editTab == 0">
|
||||
|
||||
<div class="gf-form-group section">
|
||||
<h5 class="section-heading">Details</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Name</label>
|
||||
<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.title'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Description</label>
|
||||
<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.description'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">
|
||||
Tags
|
||||
<info-popover mode="right-normal">Press enter to add a tag</info-popover>
|
||||
</label>
|
||||
<bootstrap-tagsinput ng-model="ctrl.dashboard.tags" tagclass="label label-tag" placeholder="add tags">
|
||||
</bootstrap-tagsinput>
|
||||
</div>
|
||||
<folder-picker ng-if="!ctrl.dashboard.meta.isFolder"
|
||||
initial-folder-id="ctrl.dashboard.folderId"
|
||||
on-change="ctrl.onFolderChange($folder)"
|
||||
label-class="width-7">
|
||||
</folder-picker>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5 class="section-heading">Options</h5>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-11">Timezone</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Editable"
|
||||
tooltip="Uncheck, then save and reload to disable all dashboard editing"
|
||||
checked="ctrl.dashboard.editable"
|
||||
label-class="width-11">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Hide Controls"
|
||||
tooltip="Hide row controls. Shortcut: CTRL+H or CMD+H"
|
||||
checked="ctrl.dashboard.hideControls"
|
||||
label-class="width-11">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h5 class="section-heading">Panel Options</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-11">
|
||||
Graph Tooltip
|
||||
<info-popover mode="right-normal">
|
||||
Cycle between options using Shortcut: CTRL+O or CMD+O
|
||||
</info-popover>
|
||||
</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select ng-model="ctrl.dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="editor.index == 1">
|
||||
<dash-links-editor></dash-links-editor>
|
||||
</div>
|
||||
|
||||
<div ng-if="editor.index == 2">
|
||||
<gf-time-picker-settings dashboard="ctrl.dashboard"></gf-time-picker-settings>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
const template = `
|
||||
|
||||
109
public/app/features/dashboard/settings/settings.html
Normal file
109
public/app/features/dashboard/settings/settings.html
Normal file
@@ -0,0 +1,109 @@
|
||||
<aside class="dashboard-settings__aside">
|
||||
<h2 class="dashboard-settings__aside-header">
|
||||
<i class="fa fa-cog"></i>
|
||||
Settings
|
||||
</h2>
|
||||
|
||||
<a href="{{::section.url}}" class="dashboard-settings__nav-item" ng-class="{active: ctrl.viewId === section.id}" ng-repeat="section in ctrl.sections">
|
||||
<i class="{{::section.icon}}"></i>
|
||||
{{::section.title}}
|
||||
</a>
|
||||
|
||||
<div class="dashboard-settings__aside-actions">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
|
||||
<i class="fa fa-copy"></i>
|
||||
Save As...
|
||||
</button>
|
||||
|
||||
<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
|
||||
<i class="fa fa-trash"></i>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'settings'">
|
||||
<h3 class="dashboard-settings__header">
|
||||
General
|
||||
</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Name</label>
|
||||
<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.title'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Description</label>
|
||||
<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.description'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">
|
||||
Tags
|
||||
<info-popover mode="right-normal">Press enter to add a tag</info-popover>
|
||||
</label>
|
||||
<bootstrap-tagsinput ng-model="ctrl.dashboard.tags" tagclass="label label-tag" placeholder="add tags">
|
||||
</bootstrap-tagsinput>
|
||||
</div>
|
||||
<folder-picker initial-title="ctrl.dashboard.meta.folderTitle"
|
||||
initial-folder-id="ctrl.dashboard.folderId"
|
||||
on-change="ctrl.onFolderChange($folder)"
|
||||
label-class="width-7">
|
||||
</folder-picker>
|
||||
<gf-form-switch class="gf-form" label="Editable" tooltip="Uncheck, then save and reload to disable all dashboard editing" checked="ctrl.dashboard.editable" label-class="width-7">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
|
||||
<gf-time-picker-settings dashboard="ctrl.dashboard"></gf-time-picker-settings>
|
||||
|
||||
<h5 class="section-heading">Panel Options</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-11">
|
||||
Graph Tooltip
|
||||
<info-popover mode="right-normal">
|
||||
Cycle between options using Shortcut: CTRL+O or CMD+O
|
||||
</info-popover>
|
||||
</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select ng-model="ctrl.dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'annotations'" ng-include="'public/app/features/annotations/partials/editor.html'">
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'templating'" ng-include="'public/app/features/templating/partials/editor.html'">
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'links'" >
|
||||
<dash-links-editor></dash-links-editor>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'versions'" >
|
||||
<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'view_json'" >
|
||||
<h3 class="dashboard-settings__header">View JSON</h3>
|
||||
|
||||
<div class="gf-form">
|
||||
<textarea class="gf-form-input" ng-model="ctrl.json" rows="30" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === '404'">
|
||||
<h3 class="dashboard-settings__header">Settings view not found</h3>
|
||||
|
||||
<div>
|
||||
<h5>The settings page could not be found or you do not have permission to access it</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'make_editable'">
|
||||
<h3 class="dashboard-settings__header">Make Editable</h3>
|
||||
|
||||
<button class="btn btn-success" ng-click="ctrl.makeEditable()">
|
||||
Make Editable
|
||||
</button>
|
||||
</div>
|
||||
|
||||
156
public/app/features/dashboard/settings/settings.ts
Normal file
156
public/app/features/dashboard/settings/settings.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { coreModule, appEvents, contextSrv } from 'app/core/core';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class SettingsCtrl {
|
||||
dashboard: DashboardModel;
|
||||
isOpen: boolean;
|
||||
viewId: string;
|
||||
json: string;
|
||||
alertCount: number;
|
||||
canSaveAs: boolean;
|
||||
canDelete: boolean;
|
||||
sections: any[];
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $location, private $rootScope, private backendSrv, private dashboardSrv) {
|
||||
// temp hack for annotations and variables editors
|
||||
// that rely on inherited scope
|
||||
$scope.dashboard = this.dashboard;
|
||||
|
||||
this.$scope.$on('$destroy', () => {
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
});
|
||||
|
||||
this.canSaveAs = contextSrv.isEditor;
|
||||
this.canDelete = this.dashboard.meta.canSave;
|
||||
|
||||
this.buildSectionList();
|
||||
this.onRouteUpdated();
|
||||
|
||||
$rootScope.onAppEvent('$routeUpdate', this.onRouteUpdated.bind(this), $scope);
|
||||
}
|
||||
|
||||
buildSectionList() {
|
||||
this.sections = [];
|
||||
if (this.dashboard.meta.canEdit) {
|
||||
this.sections.push({ title: 'General', id: 'settings', icon: 'fa fa-fw fa-sliders' });
|
||||
this.sections.push({ title: 'Annotations', id: 'annotations', icon: 'fa fa-fw fa-comment-o' });
|
||||
this.sections.push({ title: 'Variables', id: 'templating', icon: 'fa fa-fw fa-dollar' });
|
||||
this.sections.push({ title: 'Links', id: 'links', icon: 'fa fa-fw fa-external-link' });
|
||||
|
||||
if (this.dashboard.id) {
|
||||
this.sections.push({ title: 'Versions', id: 'versions', icon: 'fa fa-fw fa-history' });
|
||||
}
|
||||
}
|
||||
|
||||
if (contextSrv.isEditor && !this.dashboard.editable) {
|
||||
this.sections.push({ title: 'Make Editable', icon: 'fa fa-fw fa-edit', id: 'make_editable' });
|
||||
this.viewId = 'make_editable';
|
||||
}
|
||||
|
||||
this.sections.push({ title: 'View JSON', id: 'view_json', icon: 'fa fa-fw fa-code' });
|
||||
|
||||
const params = this.$location.search();
|
||||
const url = this.$location.path();
|
||||
|
||||
for (let section of this.sections) {
|
||||
const sectionParams = _.defaults({ editview: section.id }, params);
|
||||
section.url = url + '?' + $.param(sectionParams);
|
||||
}
|
||||
}
|
||||
|
||||
onRouteUpdated() {
|
||||
this.viewId = this.$location.search().editview;
|
||||
|
||||
if (this.viewId) {
|
||||
this.json = JSON.stringify(this.dashboard.getSaveModelClone(), null, 2);
|
||||
}
|
||||
|
||||
const currentSection = _.find(this.sections, { id: this.viewId });
|
||||
if (!currentSection) {
|
||||
this.sections.unshift({ title: 'Not found', id: '404', icon: 'fa fa-fw fa-warning' });
|
||||
this.viewId = '404';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
openSaveAsModal() {
|
||||
this.dashboardSrv.showSaveAsModal();
|
||||
}
|
||||
|
||||
hideSettings() {
|
||||
var urlParams = this.$location.search();
|
||||
delete urlParams.editview;
|
||||
setTimeout(() => {
|
||||
this.$rootScope.$apply(() => {
|
||||
this.$location.search(urlParams);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
makeEditable() {
|
||||
this.dashboard.editable = true;
|
||||
|
||||
return this.dashboardSrv.saveDashboard({ makeEditable: true, overwrite: false }).then(() => {
|
||||
// force refresh whole page
|
||||
window.location.href = window.location.href;
|
||||
});
|
||||
}
|
||||
|
||||
deleteDashboard() {
|
||||
var confirmText = '';
|
||||
var text2 = this.dashboard.title;
|
||||
|
||||
const alerts = _.sumBy(this.dashboard.panels, panel => {
|
||||
return panel.alert ? 1 : 0;
|
||||
});
|
||||
|
||||
if (alerts > 0) {
|
||||
confirmText = 'DELETE';
|
||||
text2 = `This dashboard contains ${alerts} alerts. Deleting this dashboard will also delete those alerts`;
|
||||
}
|
||||
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Do you want to delete this dashboard?',
|
||||
text2: text2,
|
||||
icon: 'fa-trash',
|
||||
confirmText: confirmText,
|
||||
yesText: 'Delete',
|
||||
onConfirm: () => {
|
||||
this.dashboard.meta.canSave = false;
|
||||
this.deleteDashboardConfirmed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteDashboardConfirmed() {
|
||||
this.backendSrv.delete('/api/dashboards/db/' + this.dashboard.meta.slug).then(() => {
|
||||
appEvents.emit('alert-success', ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
|
||||
this.$location.url('/');
|
||||
});
|
||||
}
|
||||
|
||||
onFolderChange(folder) {
|
||||
this.dashboard.folderId = folder.id;
|
||||
this.dashboard.meta.folderId = folder.id;
|
||||
this.dashboard.meta.folderTitle = folder.title;
|
||||
}
|
||||
}
|
||||
|
||||
export function dashboardSettings() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/settings/settings.html',
|
||||
controller: SettingsCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
transclude: true,
|
||||
scope: { dashboard: '=' },
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashboardSettings', dashboardSettings);
|
||||
@@ -1,5 +1,4 @@
|
||||
<div class="submenu-controls">
|
||||
|
||||
<div ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label template-variable" ng-hide="variable.hide === 1">
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<div class="row pull-right">
|
||||
<form name="timeForm" class="gf-timepicker-absolute-section">
|
||||
<h3>Time range</h3>
|
||||
|
||||
<label class="small">From:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.from" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openFromPicker=!openFromPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="openFromPicker">
|
||||
<datepicker ng-model="ctrl.absolute.fromJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteFromChanged()"></datepicker>
|
||||
</div>
|
||||
|
||||
|
||||
<label class="small">To:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.to" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openToPicker=!openToPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="openToPicker">
|
||||
<datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteToChanged()"></datepicker>
|
||||
</div>
|
||||
|
||||
<label class="small">Refreshing every:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<select ng-model="ctrl.refresh.value" class="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button type="submit" class="btn gf-form-btn btn-secondary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<div class="gf-timepicker-relative-section">
|
||||
<h3>Quick ranges</h3>
|
||||
<ul ng-repeat="group in ctrl.timeOptions">
|
||||
<li bindonce ng-repeat='option in group' ng-class="{active: option.active}">
|
||||
<a ng-click="ctrl.setRelativeFilter(option)" bo-text="option.display"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<div class="editor-row">
|
||||
<h5 class="section-heading">Time Options</h5>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Auto-refresh</span>
|
||||
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.refresh_intervals" array-join>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Now delay now-</span>
|
||||
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.nowDelay" placeholder="0m" valid-time-span bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'"
|
||||
data-placement="right">
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Hide time picker" checked="ctrl.panel.hidden" label-class="width-10"></gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-10">Timezone</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Auto-refresh</span>
|
||||
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.refresh_intervals" array-join>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Now delay now-</span>
|
||||
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.nowDelay" placeholder="0m" valid-time-span bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'"
|
||||
data-placement="right">
|
||||
</div>
|
||||
|
||||
<gf-form-switch class="gf-form" label="Hide time picker" checked="ctrl.panel.hidden" label-class="width-10"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,3 +24,62 @@
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.isOpen" class="gf-timepicker-dropdown">
|
||||
<form name="timeForm" class="gf-timepicker-absolute-section">
|
||||
<h3 class="section-heading">Custom range</h3>
|
||||
|
||||
<label class="small">From:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.from" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openFromPicker=!openFromPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="openFromPicker">
|
||||
<datepicker ng-model="ctrl.absolute.fromJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteFromChanged()"></datepicker>
|
||||
</div>
|
||||
|
||||
|
||||
<label class="small">To:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.to" input-datetime>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-primary" type="button" ng-click="openToPicker=!openToPicker">
|
||||
<i class="fa fa-calendar"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="openToPicker">
|
||||
<datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteToChanged()"></datepicker>
|
||||
</div>
|
||||
|
||||
<label class="small">Refreshing every:</label>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-28">
|
||||
<select ng-model="ctrl.refresh.value" class="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button type="submit" class="btn gf-form-btn btn-secondary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="gf-timepicker-relative-section">
|
||||
<h3 class="section-heading">Quick ranges</h3>
|
||||
<ul ng-repeat="group in ctrl.timeOptions">
|
||||
<li bindonce ng-repeat='option in group' ng-class="{active: option.active}">
|
||||
<a ng-click="ctrl.setRelativeFilter(option)" bo-text="option.display"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
@@ -25,10 +23,12 @@ export class TimePickerCtrl {
|
||||
refresh: any;
|
||||
isUtc: boolean;
|
||||
firstDayOfWeek: number;
|
||||
closeDropdown: any;
|
||||
isOpen: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $rootScope, private timeSrv) {
|
||||
$scope.ctrl = this;
|
||||
this.$scope.ctrl = this;
|
||||
|
||||
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
|
||||
$rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
|
||||
@@ -96,6 +96,11 @@ export class TimePickerCtrl {
|
||||
}
|
||||
|
||||
openDropdown() {
|
||||
if (this.isOpen) {
|
||||
this.isOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.onRefresh();
|
||||
this.editTimeRaw = this.timeRaw;
|
||||
this.timeOptions = rangeUtil.getRelativeTimesList(this.panel, this.rangeString);
|
||||
@@ -107,12 +112,7 @@ export class TimePickerCtrl {
|
||||
};
|
||||
|
||||
this.refresh.options.unshift({text: 'off'});
|
||||
|
||||
this.$rootScope.appEvent('show-dash-editor', {
|
||||
editview: 'timepicker',
|
||||
scope: this.$scope,
|
||||
cssClass: 'gf-timepicker-dropdown',
|
||||
});
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
applyCustom() {
|
||||
@@ -121,7 +121,7 @@ export class TimePickerCtrl {
|
||||
}
|
||||
|
||||
this.timeSrv.setTime(this.editTimeRaw);
|
||||
this.$rootScope.appEvent('hide-dash-editor');
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
absoluteFromChanged() {
|
||||
@@ -144,7 +144,7 @@ export class TimePickerCtrl {
|
||||
}
|
||||
|
||||
this.timeSrv.setTime(range);
|
||||
this.$rootScope.appEvent('hide-dash-editor');
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -175,7 +175,6 @@ export function timePickerDirective() {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
angular.module('grafana.directives').directive('gfTimePickerSettings', settingsDirective);
|
||||
angular.module('grafana.directives').directive('gfTimePicker', timePickerDirective);
|
||||
|
||||
|
||||
@@ -1,80 +1,77 @@
|
||||
<div class="editor-row">
|
||||
<h5 class="section-heading">Links and Dash Navigation</h5>
|
||||
<h3 class="dashboard-settings__header">
|
||||
Dashboard Links
|
||||
</h3>
|
||||
|
||||
<div ng-repeat="link in dashboard.links">
|
||||
|
||||
<div class="gf-form-group gf-form-inline">
|
||||
<div class="section">
|
||||
<div ng-repeat="link in dashboard.links">
|
||||
<div class="gf-form-group gf-form-inline">
|
||||
<div class="section">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Type</span>
|
||||
<div class="gf-form-select-wrapper width-10">
|
||||
<select class="gf-form-input" ng-model="link.type" ng-options="f for f in ['dashboards','link']" ng-change="updated()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="link.type === 'dashboards'">
|
||||
<span class="gf-form-label width-8">With tags</span>
|
||||
<bootstrap-tagsinput ng-model="link.tags" tagclass="label label-tag" placeholder="add tags" style="margin-right: .25rem"></bootstrap-tagsinput>
|
||||
</div>
|
||||
<gf-form-switch ng-show="link.type === 'dashboards'" class="gf-form" label="As dropdown" checked="link.asDropdown" switch-class="max-width-4" label-class="width-8" on-change="updated()"></gf-form-switch>
|
||||
<div class="gf-form" ng-show="link.type === 'dashboards' && link.asDropdown">
|
||||
<span class="gf-form-label width-8">Title</span>
|
||||
<input type="text" ng-model="link.title" class="gf-form-input max-width-10" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
<div ng-show="link.type === 'link'">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Type</span>
|
||||
<div class="gf-form-select-wrapper width-10">
|
||||
<select class="gf-form-input" ng-model="link.type" ng-options="f for f in ['dashboards','link']" ng-change="updated()"></select>
|
||||
</div>
|
||||
<li class="gf-form-label width-8">Url</li>
|
||||
<input type="text" ng-model="link.url" class="gf-form-input width-20" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
<div class="gf-form" ng-show="link.type === 'dashboards'">
|
||||
<span class="gf-form-label width-8">With tags</span>
|
||||
<bootstrap-tagsinput ng-model="link.tags" tagclass="label label-tag" placeholder="add tags" style="margin-right: .25rem"></bootstrap-tagsinput>
|
||||
</div>
|
||||
<gf-form-switch ng-show="link.type === 'dashboards'" class="gf-form" label="As dropdown" checked="link.asDropdown" switch-class="max-width-4" label-class="width-8" on-change="updated()"></gf-form-switch>
|
||||
<div class="gf-form" ng-show="link.type === 'dashboards' && link.asDropdown">
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Title</span>
|
||||
<input type="text" ng-model="link.title" class="gf-form-input max-width-10" ng-model-onblur ng-change="updated()">
|
||||
<input type="text" ng-model="link.title" class="gf-form-input width-20" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
<div ng-show="link.type === 'link'">
|
||||
<div class="gf-form">
|
||||
<li class="gf-form-label width-8">Url</li>
|
||||
<input type="text" ng-model="link.url" class="gf-form-input width-20" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Title</span>
|
||||
<input type="text" ng-model="link.title" class="gf-form-input width-20" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Tooltip</span>
|
||||
<input type="text" ng-model="link.tooltip" class="gf-form-input width-20" placeholder="Open dashboard" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Tooltip</span>
|
||||
<input type="text" ng-model="link.tooltip" class="gf-form-input width-20" placeholder="Open dashboard" ng-model-onblur ng-change="updated()">
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Icon</span>
|
||||
<div class="gf-form-select-wrapper width-20">
|
||||
<select class="gf-form-input" ng-model="link.icon" ng-options="k as k for (k, v) in iconMap" ng-change="updated()"></select>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Icon</span>
|
||||
<div class="gf-form-select-wrapper width-20">
|
||||
<select class="gf-form-input" ng-model="link.icon" ng-options="k as k for (k, v) in iconMap" ng-change="updated()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-inline" style="display: flex">
|
||||
<div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Include</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<gf-form-switch class="gf-form" label="Time range" checked="link.keepTime" switch-class="max-width-6" label-class="width-9"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Variable values" checked="link.includeVars" switch-class="max-width-6" label-class="width-9"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Open in new tab" checked="link.targetBlank" switch-class="max-width-6" label-class="width-9"></gf-form-switch>
|
||||
<div class="section gf-form-inline" style="display: flex">
|
||||
<div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Include</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<gf-form-switch class="gf-form" label="Time range" checked="link.keepTime" switch-class="max-width-6" label-class="width-9"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Variable values" checked="link.includeVars" switch-class="max-width-6" label-class="width-9"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Open in new tab" checked="link.targetBlank" switch-class="max-width-6" label-class="width-9"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; flex-direction:column; justify-content:flex-start">
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn width-4" ng-click="deleteLink($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn width-4" ng-click="moveLink($index, -1)" ng-hide="$first"><i class="fa fa-arrow-up"></i></button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn width-4" ng-click="moveLink($index, 1)" ng-hide="$last"><i class="fa fa-arrow-down"></i></button>
|
||||
</div>
|
||||
<div style="display:flex; flex-direction:column; justify-content:flex-start">
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn width-4" ng-click="deleteLink($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn width-4" ng-click="moveLink($index, -1)" ng-hide="$first"><i class="fa fa-arrow-up"></i></button>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse gf-form-btn width-4" ng-click="moveLink($index, 1)" ng-hide="$last"><i class="fa fa-arrow-down"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<button class="btn btn-inverse" ng-click="addLink()"><i class="fa fa-plus"></i> Add link</button>
|
||||
|
||||
@@ -5,7 +5,9 @@ import './select_org_ctrl';
|
||||
import './change_password_ctrl';
|
||||
import './new_org_ctrl';
|
||||
import './user_invite_ctrl';
|
||||
import './user_groups_ctrl';
|
||||
import './teams_ctrl';
|
||||
import './team_details_ctrl';
|
||||
import './create_team_modal';
|
||||
import './org_api_keys_ctrl';
|
||||
import './org_details_ctrl';
|
||||
import './prefs_control';
|
||||
|
||||
37
public/app/features/org/create_team_modal.ts
Normal file
37
public/app/features/org/create_team_modal.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class CreateTeamCtrl {
|
||||
teamName = '';
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $location) {
|
||||
}
|
||||
|
||||
createTeam() {
|
||||
this.backendSrv.post('/api/teams', {name: this.teamName}).then((result) => {
|
||||
if (result.teamId) {
|
||||
this.$location.path('/org/teams/edit/' + result.teamId);
|
||||
}
|
||||
this.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
appEvents.emit('hide-modal');
|
||||
}
|
||||
}
|
||||
|
||||
export function createTeamModal() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/org/partials/create_team.html',
|
||||
controller: CreateTeamCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('createTeamModal', createTeamModal);
|
||||
@@ -1,37 +0,0 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class CreateUserGroupCtrl {
|
||||
userGroupName = '';
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $location) {
|
||||
}
|
||||
|
||||
createUserGroup() {
|
||||
this.backendSrv.post('/api/user-groups', {name: this.userGroupName}).then((result) => {
|
||||
if (result.userGroupId) {
|
||||
this.$location.path('/org/user-groups/edit/' + result.userGroupId);
|
||||
}
|
||||
this.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
appEvents.emit('hide-modal');
|
||||
}
|
||||
}
|
||||
|
||||
export function createUserGroupModal() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/org/partials/create_user_group.html',
|
||||
controller: CreateUserGroupCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('createUserGroupModal', createUserGroupModal);
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="modal-body">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
<span class="p-l-1">Create User Group</span>
|
||||
<span class="p-l-1">Create Team</span>
|
||||
</h2>
|
||||
|
||||
<a class="modal-header-close" ng-click="ctrl.dismiss();">
|
||||
@@ -10,14 +10,14 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<form name="ctrl.createUserGroupForm" class="gf-form-group" novalidate>
|
||||
<form name="ctrl.createTeamForm" class="gf-form-group" novalidate>
|
||||
<div class="p-t-2">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-21">
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.userGroupName' required give-focus="true" placeholder="Enter User Group Name"></input>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.teamName' required give-focus="true" placeholder="Enter Team Name"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn gf-form-btn btn-success" ng-click="ctrl.createUserGroup();ctrl.dismiss();">Create</button>
|
||||
<button class="btn gf-form-btn btn-success" ng-click="ctrl.createTeam();ctrl.dismiss();">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,60 +1,49 @@
|
||||
<div class="modal-body" ng-controller="UserInviteCtrl" ng-init="init()">
|
||||
<page-header model="navModel"></page-header>
|
||||
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
Invite Users
|
||||
</h2>
|
||||
<a class="modal-header-close" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="page-container page-body" ng-cloak>
|
||||
<div class="p-b-2">
|
||||
Send invite or add existing Grafana users to the organization
|
||||
<span class="highlight-word">{{contextSrv.user.orgName}}</span>
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<form name="inviteForm">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline" ng-repeat="invite in invites">
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label">Email or Username</span>
|
||||
<input type="text" ng-model="invite.loginOrEmail" required class="gf-form-input" placeholder="email@test.com">
|
||||
</div>
|
||||
<div class="gf-form max-width-14">
|
||||
<span class="gf-form-label">Name</span>
|
||||
<input type="text" ng-model="invite.name" class="gf-form-input" placeholder="name (optional)">
|
||||
</div>
|
||||
<div class="gf-form max-width-10">
|
||||
<span class="gf-form-label">Role</span>
|
||||
<select ng-model="invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
|
||||
</select>
|
||||
</div>
|
||||
<div class="gf-form gf-size-auto">
|
||||
<a class="gf-form-label pointer" tabindex="1" ng-click="removeInvite(invite)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-tagline p-b-2">
|
||||
Send invite or add existing Grafana users to the organization
|
||||
<span class="highlight-word">{{contextSrv.user.orgName}}</span>
|
||||
</div>
|
||||
<div class="gf-form-inline gf-form-group">
|
||||
<div class="gf-form" style="margin-right:.25rem">
|
||||
<a class="btn btn-inverse gf-form-button" ng-click="addInvite()">
|
||||
<i class="fa fa-plus"></i>
|
||||
Invite another
|
||||
</a>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Skip sending invite email" checked="options.skipEmails" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<form name="inviteForm">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline" ng-repeat="invite in invites">
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label">Email or Username</span>
|
||||
<input type="text" ng-model="invite.loginOrEmail" required class="gf-form-input" placeholder="email@test.com">
|
||||
</div>
|
||||
<div class="gf-form max-width-14">
|
||||
<span class="gf-form-label">Name</span>
|
||||
<input type="text" ng-model="invite.name" class="gf-form-input" placeholder="name (optional)">
|
||||
</div>
|
||||
<div class="gf-form max-width-10">
|
||||
<span class="gf-form-label">Role</span>
|
||||
<select ng-model="invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']">
|
||||
</select>
|
||||
</div>
|
||||
<div class="gf-form gf-size-auto">
|
||||
<a class="gf-form-label pointer" tabindex="1" ng-click="removeInvite(invite)">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline gf-form-group">
|
||||
<div class="gf-form" style="margin-right:.25rem">
|
||||
<a class="btn btn-inverse gf-form-button" ng-click="addInvite()">
|
||||
<i class="fa fa-plus"></i>
|
||||
Invite another
|
||||
</a>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form" label="Skip sending invite email" checked="options.skipEmails" switch-class="max-width-6"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="sendInvites();">Invite Users</button>
|
||||
<a class="btn-text" ng-click="dismiss()">Cancel</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn btn-success" ng-click="sendInvites();">Invite Users</button>
|
||||
<a class="btn-text" href="org/users">Cancel</a>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
<button class="btn btn-inverse" ng-show="ctrl.pendingInvites.length" ng-click="ctrl.editor.index = 1">
|
||||
Pending Invites ({{ctrl.pendingInvites.length}})
|
||||
</button>
|
||||
<button class="btn btn-success" ng-click="ctrl.openAddUsersView()" ng-hide="ctrl.externalUserMngLinkUrl">
|
||||
<a class="btn btn-success" href="org/users/new" ng-hide="ctrl.externalUserMngLinkUrl">
|
||||
<i class="fa fa-plus"></i>
|
||||
<span>{{ctrl.addUsersBtnName}}</span>
|
||||
</button>
|
||||
</a>
|
||||
<a class="btn btn-inverse" ng-href="{{ctrl.externalUserMngLinkUrl}}" target="_blank" ng-if="ctrl.externalUserMngLinkUrl">
|
||||
<i class="fa fa-external-link-square"></i>
|
||||
{{ctrl.addUsersBtnName}}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>Edit User Group</h1>
|
||||
<h1>Edit Team</h1>
|
||||
</div>
|
||||
|
||||
<form name="userGroupDetailsForm" class="gf-form-group gf-form-inline">
|
||||
<form name="teamDetailsForm" class="gf-form-group gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Name</span>
|
||||
<input type="text" required ng-model="ctrl.userGroup.name" class="gf-form-input max-width-14" >
|
||||
<input type="text" required ng-model="ctrl.team.name" class="gf-form-input max-width-14" >
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
@@ -17,7 +17,7 @@
|
||||
</form>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h3 class="page-heading">User Group Members</h3>
|
||||
<h3 class="page-heading">Team Members</h3>
|
||||
|
||||
<form name="ctrl.addMemberForm" class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
@@ -26,24 +26,24 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<table class="grafana-options-table" ng-show="ctrl.userGroupMembers.length > 0">
|
||||
<table class="grafana-options-table" ng-show="ctrl.teamMembers.length > 0">
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ng-repeat="member in ctrl.userGroupMembers">
|
||||
<tr ng-repeat="member in ctrl.teamMembers">
|
||||
<td>{{member.login}}</td>
|
||||
<td>{{member.email}}</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="ctrl.removeUserGroupMember(member)" class="btn btn-danger btn-mini">
|
||||
<a ng-click="ctrl.removeTeamMember(member)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div>
|
||||
<em class="muted" ng-hide="ctrl.userGroupMembers.length > 0">
|
||||
This user group has no members yet.
|
||||
<em class="muted" ng-hide="ctrl.teamMembers.length > 0">
|
||||
This team has no members yet.
|
||||
</em>
|
||||
</div>
|
||||
@@ -5,20 +5,20 @@
|
||||
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label">Search</label>
|
||||
<input type="text" class="gf-form-input max-width-20" placeholder="Find User Group by name" tabindex="1" give-focus="true"
|
||||
<input type="text" class="gf-form-input max-width-20" placeholder="Find Team by name" tabindex="1" give-focus="true"
|
||||
ng-model="ctrl.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.get()" />
|
||||
</div>
|
||||
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
|
||||
<a class="btn btn-success" ng-click="ctrl.openUserGroupModal()">
|
||||
<a class="btn btn-success" ng-click="ctrl.openTeamModal()">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add Team
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-list-table">
|
||||
<table class="filter-table form-inline" ng-show="ctrl.userGroups.length > 0">
|
||||
<table class="filter-table form-inline" ng-show="ctrl.teams.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
@@ -27,18 +27,18 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="userGroup in ctrl.userGroups">
|
||||
<tr ng-repeat="team in ctrl.teams">
|
||||
<td>
|
||||
<a href="org/user-groups/edit/{{userGroup.id}}">{{userGroup.name}}</a>
|
||||
<a href="org/teams/edit/{{team.id}}">{{team.name}}</a>
|
||||
</td>
|
||||
<td>#Count</td>
|
||||
<td class="text-right">
|
||||
<a href="org/user-groups/edit/{{userGroup.id}}" class="btn btn-inverse btn-small">
|
||||
<a href="org/teams/edit/{{team.id}}" class="btn btn-inverse btn-small">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
<a ng-click="ctrl.deleteUserGroup(userGroup)" class="btn btn-danger btn-small">
|
||||
<a ng-click="ctrl.deleteTeam(team)" class="btn btn-danger btn-small">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
@@ -58,7 +58,7 @@
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<em class="muted" ng-hide="ctrl.userGroups.length > 0">
|
||||
No User Groups found.
|
||||
<em class="muted" ng-hide="ctrl.teams.length > 0">
|
||||
No Teams found.
|
||||
</em>
|
||||
</div>
|
||||
@@ -1,8 +1,8 @@
|
||||
import '../user_group_details_ctrl';
|
||||
import '../team_details_ctrl';
|
||||
import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/common';
|
||||
import UserGroupDetailsCtrl from '../user_group_details_ctrl';
|
||||
import TeamDetailsCtrl from '../team_details_ctrl';
|
||||
|
||||
describe('UserGroupDetailsCtrl', () => {
|
||||
describe('TeamDetailsCtrl', () => {
|
||||
var ctx: any = {};
|
||||
var backendSrv = {
|
||||
searchUsers: sinon.stub().returns(Promise.resolve([])),
|
||||
@@ -16,7 +16,7 @@ var backendSrv = {
|
||||
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
|
||||
ctx.$q = $q;
|
||||
ctx.scope = $rootScope.$new();
|
||||
ctx.ctrl = $controller(UserGroupDetailsCtrl, {
|
||||
ctx.ctrl = $controller(TeamDetailsCtrl, {
|
||||
$scope: ctx.scope,
|
||||
backendSrv: backendSrv,
|
||||
$routeParams: {id: 1},
|
||||
@@ -24,7 +24,7 @@ var backendSrv = {
|
||||
});
|
||||
}));
|
||||
|
||||
describe('when user is chosen to be added to user group', () => {
|
||||
describe('when user is chosen to be added to team', () => {
|
||||
beforeEach(() => {
|
||||
const userItem = {
|
||||
id: 2,
|
||||
@@ -34,13 +34,13 @@ var backendSrv = {
|
||||
});
|
||||
|
||||
it('should parse the result and save to db', () => {
|
||||
expect(backendSrv.post.getCall(0).args[0]).to.eql('/api/user-groups/1/members');
|
||||
expect(backendSrv.post.getCall(0).args[0]).to.eql('/api/teams/1/members');
|
||||
expect(backendSrv.post.getCall(0).args[1].userId).to.eql(2);
|
||||
});
|
||||
|
||||
it('should refresh the list after saving.', () => {
|
||||
expect(backendSrv.get.getCall(0).args[0]).to.eql('/api/user-groups/1');
|
||||
expect(backendSrv.get.getCall(1).args[0]).to.eql('/api/user-groups/1/members');
|
||||
expect(backendSrv.get.getCall(0).args[0]).to.eql('/api/teams/1');
|
||||
expect(backendSrv.get.getCall(1).args[0]).to.eql('/api/teams/1/members');
|
||||
});
|
||||
});
|
||||
});
|
||||
77
public/app/features/org/team_details_ctrl.ts
Normal file
77
public/app/features/org/team_details_ctrl.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export default class TeamDetailsCtrl {
|
||||
team: Team;
|
||||
teamMembers: User[] = [];
|
||||
navModel: any;
|
||||
|
||||
constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'users');
|
||||
this.get();
|
||||
}
|
||||
|
||||
get() {
|
||||
if (this.$routeParams && this.$routeParams.id) {
|
||||
this.backendSrv.get(`/api/teams/${this.$routeParams.id}`)
|
||||
.then(result => {
|
||||
this.team = result;
|
||||
});
|
||||
this.backendSrv.get(`/api/teams/${this.$routeParams.id}/members`)
|
||||
.then(result => {
|
||||
this.teamMembers = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeTeamMember(teamMember: TeamMember) {
|
||||
this.$scope.appEvent('confirm-modal', {
|
||||
title: 'Remove Member',
|
||||
text: 'Are you sure you want to remove ' + teamMember.name + ' from this group?',
|
||||
yesText: "Remove",
|
||||
icon: "fa-warning",
|
||||
onConfirm: () => {
|
||||
this.removeMemberConfirmed(teamMember);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMemberConfirmed(teamMember: TeamMember) {
|
||||
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`)
|
||||
.then(this.get.bind(this));
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.$scope.teamDetailsForm.$valid) { return; }
|
||||
|
||||
this.backendSrv.put('/api/teams/' + this.team.id, {name: this.team.name});
|
||||
}
|
||||
|
||||
userPicked(user) {
|
||||
this.backendSrv.post(`/api/teams/${this.$routeParams.id}/members`, {userId: user.id}).then(() => {
|
||||
this.$scope.$broadcast('user-picker-reset');
|
||||
this.get();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
login: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface TeamMember {
|
||||
userId: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
coreModule.controller('TeamDetailsCtrl', TeamDetailsCtrl);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {appEvents} from 'app/core/core';
|
||||
|
||||
export class UserGroupsCtrl {
|
||||
userGroups: any;
|
||||
export class TeamsCtrl {
|
||||
teams: any;
|
||||
pages = [];
|
||||
perPage = 50;
|
||||
page = 1;
|
||||
@@ -20,9 +20,9 @@ export class UserGroupsCtrl {
|
||||
}
|
||||
|
||||
get() {
|
||||
this.backendSrv.get(`/api/user-groups/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
|
||||
this.backendSrv.get(`/api/teams/search?perpage=${this.perPage}&page=${this.page}&query=${this.query}`)
|
||||
.then((result) => {
|
||||
this.userGroups = result.userGroups;
|
||||
this.teams = result.teams;
|
||||
this.page = result.page;
|
||||
this.perPage = result.perPage;
|
||||
this.totalPages = Math.ceil(result.totalCount / result.perPage);
|
||||
@@ -40,29 +40,29 @@ export class UserGroupsCtrl {
|
||||
this.get();
|
||||
}
|
||||
|
||||
deleteUserGroup(userGroup) {
|
||||
deleteTeam(team) {
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete',
|
||||
text: 'Are you sure you want to delete User Group ' + userGroup.name + '?',
|
||||
text: 'Are you sure you want to delete Team ' + team.name + '?',
|
||||
yesText: "Delete",
|
||||
icon: "fa-warning",
|
||||
onConfirm: () => {
|
||||
this.deleteUserGroupConfirmed(userGroup);
|
||||
this.deleteTeamConfirmed(team);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteUserGroupConfirmed(userGroup) {
|
||||
this.backendSrv.delete('/api/user-groups/' + userGroup.id)
|
||||
deleteTeamConfirmed(team) {
|
||||
this.backendSrv.delete('/api/teams/' + team.id)
|
||||
.then(this.get.bind(this));
|
||||
}
|
||||
|
||||
openUserGroupModal() {
|
||||
openTeamModal() {
|
||||
appEvents.emit('show-modal', {
|
||||
templateHtml: '<create-user-group-modal></create-user-group-modal>',
|
||||
templateHtml: '<create-team-modal></create-team-modal>',
|
||||
modalClass: 'modal--narrow'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('UserGroupsCtrl', UserGroupsCtrl);
|
||||
coreModule.controller('TeamsCtrl', TeamsCtrl);
|
||||
@@ -1,77 +0,0 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export default class UserGroupDetailsCtrl {
|
||||
userGroup: UserGroup;
|
||||
userGroupMembers: User[] = [];
|
||||
navModel: any;
|
||||
|
||||
constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'users');
|
||||
this.get();
|
||||
}
|
||||
|
||||
get() {
|
||||
if (this.$routeParams && this.$routeParams.id) {
|
||||
this.backendSrv.get(`/api/user-groups/${this.$routeParams.id}`)
|
||||
.then(result => {
|
||||
this.userGroup = result;
|
||||
});
|
||||
this.backendSrv.get(`/api/user-groups/${this.$routeParams.id}/members`)
|
||||
.then(result => {
|
||||
this.userGroupMembers = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
removeUserGroupMember(userGroupMember: UserGroupMember) {
|
||||
this.$scope.appEvent('confirm-modal', {
|
||||
title: 'Remove Member',
|
||||
text: 'Are you sure you want to remove ' + userGroupMember.name + ' from this group?',
|
||||
yesText: "Remove",
|
||||
icon: "fa-warning",
|
||||
onConfirm: () => {
|
||||
this.removeMemberConfirmed(userGroupMember);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeMemberConfirmed(userGroupMember: UserGroupMember) {
|
||||
this.backendSrv.delete(`/api/user-groups/${this.$routeParams.id}/members/${userGroupMember.userId}`)
|
||||
.then(this.get.bind(this));
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.$scope.userGroupDetailsForm.$valid) { return; }
|
||||
|
||||
this.backendSrv.put('/api/user-groups/' + this.userGroup.id, {name: this.userGroup.name});
|
||||
}
|
||||
|
||||
userPicked(user) {
|
||||
this.backendSrv.post(`/api/user-groups/${this.$routeParams.id}/members`, {userId: user.id}).then(() => {
|
||||
this.$scope.$broadcast('user-picker-reset');
|
||||
this.get();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export interface UserGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
login: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface UserGroupMember {
|
||||
userId: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
coreModule.controller('UserGroupDetailsCtrl', UserGroupDetailsCtrl);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class UserInviteCtrl {
|
||||
|
||||
/** @ngInject **/
|
||||
constructor($scope, backendSrv) {
|
||||
$scope.invites = [
|
||||
constructor($scope, backendSrv, navModelSrv) {
|
||||
$scope.navModel = navModelSrv.getNav('cfg', 'users', 0);
|
||||
|
||||
const defaultInvites = [
|
||||
{name: '', email: '', role: 'Editor'},
|
||||
];
|
||||
|
||||
$scope.invites = _.cloneDeep(defaultInvites);
|
||||
|
||||
$scope.options = {skipEmails: false};
|
||||
$scope.init = function() { };
|
||||
|
||||
@@ -20,11 +24,19 @@ export class UserInviteCtrl {
|
||||
$scope.invites = _.without($scope.invites, invite);
|
||||
};
|
||||
|
||||
$scope.resetInvites = function() {
|
||||
$scope.invites = _.cloneDeep(defaultInvites);
|
||||
};
|
||||
|
||||
$scope.sendInvites = function() {
|
||||
if (!$scope.inviteForm.$valid) { return; }
|
||||
$scope.sendSingleInvite(0);
|
||||
};
|
||||
|
||||
$scope.invitesSent = function() {
|
||||
$scope.resetInvites();
|
||||
};
|
||||
|
||||
$scope.sendSingleInvite = function(index) {
|
||||
var invite = $scope.invites[index];
|
||||
invite.skipEmails = $scope.options.skipEmails;
|
||||
@@ -34,7 +46,6 @@ export class UserInviteCtrl {
|
||||
|
||||
if (index === $scope.invites.length) {
|
||||
$scope.invitesSent();
|
||||
$scope.dismiss();
|
||||
} else {
|
||||
$scope.sendSingleInvite(index);
|
||||
}
|
||||
@@ -43,4 +54,4 @@ export class UserInviteCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('grafana.controllers').controller('UserInviteCtrl', UserInviteCtrl);
|
||||
coreModule.controller('UserInviteCtrl', UserInviteCtrl);
|
||||
|
||||
@@ -2,6 +2,7 @@ import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import {appEvents, profiler} from 'app/core/core';
|
||||
import { PanelModel } from 'app/features/dashboard/panel_model';
|
||||
import Remarkable from 'remarkable';
|
||||
import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants';
|
||||
|
||||
@@ -134,20 +135,27 @@ export class PanelCtrl {
|
||||
getMenu() {
|
||||
let menu = [];
|
||||
menu.push({text: 'View', click: 'ctrl.viewPanel();', icon: "fa fa-fw fa-eye", shortcut: "v"});
|
||||
menu.push({text: 'Edit', click: 'ctrl.editPanel();', role: 'Editor', icon: "fa fa-fw fa-edit", shortcut: "e"});
|
||||
|
||||
if (this.dashboard.meta.canEdit) {
|
||||
menu.push({text: 'Edit', click: 'ctrl.editPanel();', role: 'Editor', icon: "fa fa-fw fa-edit", shortcut: "e"});
|
||||
}
|
||||
|
||||
menu.push({text: 'Share', click: 'ctrl.sharePanel();', icon: "fa fa-fw fa-share", shortcut: "p s"});
|
||||
|
||||
let extendedMenu = this.getExtendedMenu();
|
||||
menu.push({text: 'More ...', click: 'ctrl.removePanel();', icon: "fa fa-fw fa-cube", submenu: extendedMenu});
|
||||
|
||||
menu.push({divider: true, role: 'Editor'});
|
||||
menu.push({text: 'Remove', click: 'ctrl.removePanel();', role: 'Editor', icon: "fa fa-fw fa-trash", shortcut: "p r"});
|
||||
if (this.dashboard.meta.canEdit) {
|
||||
menu.push({divider: true, role: 'Editor'});
|
||||
menu.push({text: 'Remove', click: 'ctrl.removePanel();', role: 'Editor', icon: "fa fa-fw fa-trash", shortcut: "p r"});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
getExtendedMenu() {
|
||||
let menu = [];
|
||||
if (!this.fullscreen) {
|
||||
if (!this.fullscreen && this.dashboard.meta.canEdit) {
|
||||
menu.push({ text: 'Duplicate', click: 'ctrl.duplicate()', role: 'Editor' });
|
||||
}
|
||||
menu.push({text: 'Panel JSON', click: 'ctrl.editPanelJson(); dismiss();' });
|
||||
@@ -216,22 +224,31 @@ export class PanelCtrl {
|
||||
}
|
||||
|
||||
editPanelJson() {
|
||||
this.publishAppEvent('show-json-editor', {
|
||||
object: this.panel.getSaveModel(),
|
||||
updateHandler: this.replacePanel.bind(this)
|
||||
let editScope = this.$scope.$root.$new();
|
||||
editScope.object = this.panel.getSaveModel();
|
||||
editScope.updateHandler = this.replacePanel.bind(this);
|
||||
|
||||
this.publishAppEvent('show-modal', {
|
||||
src: 'public/app/partials/edit_json.html',
|
||||
scope: editScope
|
||||
});
|
||||
}
|
||||
|
||||
replacePanel(newPanel, oldPanel) {
|
||||
var index = _.indexOf(this.dashboard.panels, oldPanel);
|
||||
this.dashboard.panels.splice(index, 1);
|
||||
|
||||
// adding it back needs to be done in next digest
|
||||
this.$timeout(() => {
|
||||
newPanel.id = oldPanel.id;
|
||||
newPanel.width = oldPanel.width;
|
||||
this.dashboard.panels.splice(index, 0, newPanel);
|
||||
let dashboard = this.dashboard;
|
||||
let index = _.findIndex(dashboard.panels, (panel) => {
|
||||
return panel.id === oldPanel.id;
|
||||
});
|
||||
|
||||
let deletedPanel = dashboard.panels.splice(index, 1);
|
||||
this.dashboard.events.emit('panel-removed', deletedPanel);
|
||||
|
||||
newPanel = new PanelModel(newPanel);
|
||||
newPanel.id = oldPanel.id;
|
||||
|
||||
dashboard.panels.splice(index, 0, newPanel);
|
||||
dashboard.sortPanelsByGridPos();
|
||||
dashboard.events.emit('panel-added', newPanel);
|
||||
}
|
||||
|
||||
sharePanel() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import config from 'app/core/config';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import {variableTypes} from './variable';
|
||||
@@ -45,6 +43,10 @@ export class VariableEditorCtrl {
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setMode = function(mode) {
|
||||
$scope.mode = mode;
|
||||
};
|
||||
|
||||
$scope.add = function() {
|
||||
if ($scope.isValid()) {
|
||||
variableSrv.addVariable($scope.current);
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
<div ng-controller="VariableEditorCtrl" ng-init="init()">
|
||||
<div class="tabbed-view-header">
|
||||
<h2 class="tabbed-view-title">
|
||||
Templating
|
||||
</h2>
|
||||
|
||||
<ul class="gf-tabs">
|
||||
<li class="gf-tabs-item" >
|
||||
<a class="gf-tabs-link" ng-click="mode = 'list';" ng-class="{active: mode === 'list'}">
|
||||
Variables
|
||||
</a>
|
||||
</li>
|
||||
<li class="gf-tabs-item" ng-show="mode === 'edit'">
|
||||
<a class="gf-tabs-link" ng-class="{active: mode === 'edit'}">
|
||||
Edit
|
||||
</a>
|
||||
</li>
|
||||
<li class="gf-tabs-item" ng-show="mode === 'new'">
|
||||
<span class="active gf-tabs-link">New</span>
|
||||
</li>
|
||||
<li class="gf-tabs-item" >
|
||||
<a class="gf-tabs-link" ng-click="mode = 'help';" ng-class="{active: mode === 'help'}">
|
||||
Help
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 class="dashboard-settings__header">
|
||||
<a ng-click="setMode('list')">Variables</a>
|
||||
<span ng-show="mode === 'new'">> New</span>
|
||||
<span ng-show="mode === 'edit'">> Edit</span>
|
||||
</h3>
|
||||
|
||||
<button class="tabbed-view-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div ng-if="mode === 'list'">
|
||||
|
||||
<div class="tabbed-view-body">
|
||||
<div ng-if="variables.length === 0">
|
||||
<div class="empty-list-cta">
|
||||
<div class="empty-list-cta__title">There are no variables added yet</div>
|
||||
<a ng-click="setMode('new')" class="empty-list-cta__button btn btn-xlarge btn-success">
|
||||
<i class="gicon gicon-dashboard-new"></i>
|
||||
Add variable
|
||||
</a>
|
||||
<div class="grafana-info-box">
|
||||
<h5>What does variables do?</h5>
|
||||
<p>Variables enables more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names
|
||||
in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
|
||||
the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
|
||||
|
||||
<div ng-if="mode === 'list'">
|
||||
<div ng-if="variables.length === 0">
|
||||
<em>No template variables defined</em>
|
||||
<br /> <br />
|
||||
Checkout the
|
||||
<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
|
||||
Templating documentation
|
||||
</a> for more information.
|
||||
</div>
|
||||
</div>
|
||||
<table class="filter-table filter-table--hover">
|
||||
<thead>
|
||||
<tr>
|
||||
</div>
|
||||
|
||||
<div ng-if="variables.length">
|
||||
<div class="page-action-bar">
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a type="button" class="btn btn-success" ng-click="setMode('new');"><i class="fa fa-plus" ></i> New</a>
|
||||
</div>
|
||||
|
||||
<table class="filter-table filter-table--hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Definition</th>
|
||||
<th colspan="5"></th>
|
||||
@@ -55,7 +53,6 @@
|
||||
<td style="max-width: 200px;" ng-click="edit(variable)" class="pointer max-width">
|
||||
{{variable.query}}
|
||||
</td>
|
||||
|
||||
<td style="width: 1%"><i ng-click="_.move(variables,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
|
||||
<td style="width: 1%"><i ng-click="_.move(variables,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
|
||||
<td style="width: 1%">
|
||||
@@ -70,256 +67,236 @@
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="mode === 'help'">
|
||||
<div class="grafana-info-box col-lg-8">
|
||||
<h5>What does templating do?</h5>
|
||||
<p>Templating allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application
|
||||
and sensor name in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
|
||||
the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
|
||||
<br>
|
||||
<br>
|
||||
<form ng-if="mode === 'edit' || mode === 'new'" name="ctrl.form">
|
||||
<h5 class="section-heading">General</h5>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input" name="name" placeholder="name" ng-model='current.name' required ng-pattern="namePattern"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">
|
||||
Type
|
||||
<info-popover mode="right-normal">
|
||||
{{variableTypes[current.type].description}}
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-17">
|
||||
<select class="gf-form-input" ng-model="current.type" ng-options="k as v.name for (k, v) in variableTypes" ng-change="typeChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/templating/">Templating documentation</a> for more information.
|
||||
</p>
|
||||
<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
|
||||
<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__' that's reserved for Grafanas global variables</span>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">Label</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.label' placeholder="optional display name"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">Hide</span>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="current.hide" ng-options="f.value as f.text for f in hideOptions"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="mode === 'list'">
|
||||
<div class="gf-form-button-row">
|
||||
<a type="button" class="btn gf-form-button btn-success" ng-click="mode = 'new';"><i class="fa fa-plus" ></i> New</a>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="current.type === 'interval'" class="gf-form-group">
|
||||
<h5 class="section-heading">Interval Options</h5>
|
||||
|
||||
<form ng-if="mode === 'edit' || mode === 'new'" name="ctrl.form">
|
||||
<h5 class="section-heading">Variable</h5>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input" name="name" placeholder="name" ng-model='current.name' required ng-pattern="namePattern"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">
|
||||
Type
|
||||
<info-popover mode="right-normal">
|
||||
{{variableTypes[current.type].description}}
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-17">
|
||||
<select class="gf-form-input" ng-model="current.type" ng-options="k as v.name for (k, v) in variableTypes" ng-change="typeChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Values</span>
|
||||
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()" required></input>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
|
||||
<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__' that's reserved for Grafanas global variables</span>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label="Auto Option" label-class="width-9" checked="current.auto" on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">Label</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.label' placeholder="optional display name"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-19">
|
||||
<span class="gf-form-label width-6">Hide</span>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="current.hide" ng-options="f.value as f.text for f in hideOptions"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9" ng-show="current.auto">
|
||||
Step count <tip>How many times should the current time range be divided to calculate the value</tip>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-10" ng-show="current.auto">
|
||||
<select class="gf-form-input" ng-model="current.auto_count" ng-options="f for f in [1,2,3,4,5,10,20,30,40,50,100,200,300,400,500]" ng-change="runQuery()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label" ng-show="current.auto">
|
||||
Min interval <tip>The calculated value will not go below this threshold</tip>
|
||||
</span>
|
||||
<input type="text" class="gf-form-input max-width-10" ng-show="current.auto" ng-model="current.auto_min" ng-change="runQuery()" placeholder="10s"></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'interval'" class="gf-form-group">
|
||||
<h5 class="section-heading">Interval Options</h5>
|
||||
<div ng-if="current.type === 'custom'" class="gf-form-group">
|
||||
<h5 class="section-heading">Custom Options</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Values separated by comma</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Values</span>
|
||||
<input type="text" class="gf-form-input" placeholder="name" ng-model='current.query' placeholder="1m,10m,1h,6h,1d,7d" ng-model-onblur ng-change="runQuery()" required></input>
|
||||
</div>
|
||||
<div ng-if="current.type === 'constant'" class="gf-form-group">
|
||||
<h5 class="section-heading">Constant options</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Value</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="your metric prefix"></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label="Auto Option" label-class="width-9" checked="current.auto" on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
<div ng-if="current.type === 'query'" class="gf-form-group">
|
||||
<h5 class="section-heading">Query Options</h5>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9" ng-show="current.auto">
|
||||
Step count <tip>How many times should the current time range be divided to calculate the value</tip>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-10" ng-show="current.auto">
|
||||
<select class="gf-form-input" ng-model="current.auto_count" ng-options="f for f in [1,2,3,4,5,10,20,30,40,50,100,200,300,400,500]" ng-change="runQuery()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label" ng-show="current.auto">
|
||||
Min interval <tip>The calculated value will not go below this threshold</tip>
|
||||
</span>
|
||||
<input type="text" class="gf-form-input max-width-10" ng-show="current.auto" ng-model="current.auto_min" ng-change="runQuery()" placeholder="10s"></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-7">Data source</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required>
|
||||
<option value="" ng-if="false"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-7">
|
||||
Refresh
|
||||
<info-popover mode="right-normal">
|
||||
When to update the values of this variable.
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select class="gf-form-input" ng-model="current.refresh" ng-options="f.value as f.text for f in refreshOptions"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()" required></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">
|
||||
Regex
|
||||
<info-popover mode="right-normal">
|
||||
Optional, if you want to extract part of a series name or metric node segment.
|
||||
</info-popover>
|
||||
</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-7">
|
||||
Sort
|
||||
<info-popover mode="right-normal">
|
||||
How to sort the values of this variable.
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select class="gf-form-input" ng-model="current.sort" ng-options="f.value as f.text for f in sortOptions" ng-change="runQuery()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'custom'" class="gf-form-group">
|
||||
<h5 class="section-heading">Custom Options</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Values separated by comma</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="current.type === 'datasource'" class="gf-form-group">
|
||||
<h5 class="section-heading">Data source options</h5>
|
||||
|
||||
<div ng-if="current.type === 'constant'" class="gf-form-group">
|
||||
<h5 class="section-heading">Constant options</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label">Value</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="your metric prefix"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-12">Type</label>
|
||||
<div class="gf-form-select-wrapper max-width-18">
|
||||
<select class="gf-form-input" ng-model="current.query" ng-options="f.value as f.text for f in datasourceTypes" ng-change="runQuery()"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'query'" class="gf-form-group">
|
||||
<h5 class="section-heading">Query Options</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-12">
|
||||
Instance name filter
|
||||
<info-popover mode="right-normal">
|
||||
Regex filter for which data source instances to choose from in
|
||||
the variable value dropdown. Leave empty for all.
|
||||
<br><br>
|
||||
Example: <code>/^prod/</code>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-7">Data source</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required>
|
||||
<option value="" ng-if="false"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-7">
|
||||
Refresh
|
||||
<info-popover mode="right-normal">
|
||||
When to update the values of this variable.
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select class="gf-form-input" ng-model="current.refresh" ng-options="f.value as f.text for f in refreshOptions"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.query' placeholder="metric name or tags query" ng-model-onblur ng-change="runQuery()" required></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">
|
||||
Regex
|
||||
<info-popover mode="right-normal">
|
||||
Optional, if you want to extract part of a series name or metric node segment.
|
||||
</info-popover>
|
||||
</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
|
||||
</div>
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-7">
|
||||
Sort
|
||||
<info-popover mode="right-normal">
|
||||
How to sort the values of this variable.
|
||||
</info-popover>
|
||||
</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select class="gf-form-input" ng-model="current.sort" ng-options="f.value as f.text for f in sortOptions" ng-change="runQuery()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</info-popover>
|
||||
</label>
|
||||
<input type="text" class="gf-form-input max-width-18" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="current.type === 'datasource'" class="gf-form-group">
|
||||
<h5 class="section-heading">Data source options</h5>
|
||||
<div ng-if="current.type === 'adhoc'" class="gf-form-group">
|
||||
<h5 class="section-heading">Options</h5>
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-8">Data source</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required ng-change="validate()">
|
||||
<option value="" ng-if="false"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-12">Type</label>
|
||||
<div class="gf-form-select-wrapper max-width-18">
|
||||
<select class="gf-form-input" ng-model="current.query" ng-options="f.value as f.text for f in datasourceTypes" ng-change="runQuery()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section gf-form-group" ng-show="variableTypes[current.type].supportsMulti">
|
||||
<h5 class="section-heading">Selection Options</h5>
|
||||
<div class="section">
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Multi-value"
|
||||
label-class="width-10"
|
||||
tooltip="Enables multiple values to be selected at the same time"
|
||||
checked="current.multi"
|
||||
on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Include All option"
|
||||
label-class="width-10"
|
||||
checked="current.includeAll"
|
||||
on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.includeAll">
|
||||
<span class="gf-form-label width-10">Custom all value</span>
|
||||
<input type="text" class="gf-form-input max-width-15" ng-model='current.allValue' placeholder="blank = auto"></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-12">
|
||||
Instance name filter
|
||||
<info-popover mode="right-normal">
|
||||
Regex filter for which data source instances to choose from in
|
||||
the variable value dropdown. Leave empty for all.
|
||||
<br><br>
|
||||
Example: <code>/^prod/</code>
|
||||
<div class="gf-form-group" ng-if="current.type === 'query'">
|
||||
<h5>Value groups/tags (Experimental feature)</h5>
|
||||
<gf-form-switch class="gf-form" label="Enabled" label-class="width-10" checked="current.useTags" on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form last" ng-if="current.useTags">
|
||||
<span class="gf-form-label width-10">Tags query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.tagsQuery' placeholder="metric name or tags query" ng-model-onblur></input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.useTags">
|
||||
<li class="gf-form-label width-10">Tag values query</li>
|
||||
<input type="text" class="gf-form-input" ng-model='current.tagValuesQuery' placeholder="apps.$tag.*" ng-model-onblur></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</info-popover>
|
||||
</label>
|
||||
<input type="text" class="gf-form-input max-width-18" ng-model='current.regex' placeholder="/.*-(.*)-.*/" ng-model-onblur ng-change="runQuery()"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-show="current.options.length">
|
||||
<h5>Preview of values (shows max 20)</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-repeat="option in current.options | limitTo: 20">
|
||||
<span class="gf-form-label">{{option.text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="current.type === 'adhoc'" class="gf-form-group">
|
||||
<h5 class="section-heading">Options</h5>
|
||||
<div class="gf-form max-width-21">
|
||||
<span class="gf-form-label width-8">Data source</span>
|
||||
<div class="gf-form-select-wrapper max-width-14">
|
||||
<select class="gf-form-input" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources" required ng-change="validate()">
|
||||
<option value="" ng-if="false"></option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info gf-form-group" ng-if="infoText">
|
||||
{{infoText}}
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group" ng-show="variableTypes[current.type].supportsMulti">
|
||||
<h5 class="section-heading">Selection Options</h5>
|
||||
<div class="section">
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Multi-value"
|
||||
label-class="width-10"
|
||||
tooltip="Enables multiple values to be selected at the same time"
|
||||
checked="current.multi"
|
||||
on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Include All option"
|
||||
label-class="width-10"
|
||||
checked="current.includeAll"
|
||||
on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.includeAll">
|
||||
<span class="gf-form-label width-10">Custom all value</span>
|
||||
<input type="text" class="gf-form-input max-width-15" ng-model='current.allValue' placeholder="blank = auto"></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button type="submit" class="btn btn-success" ng-show="mode === 'edit'" ng-click="update();">Update</button>
|
||||
<button type="submit" class="btn btn-success" ng-show="mode === 'new'" ng-click="add();">Add</button>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="current.type === 'query'">
|
||||
<h5>Value groups/tags (Experimental feature)</h5>
|
||||
<gf-form-switch class="gf-form" label="Enabled" label-class="width-10" checked="current.useTags" on-change="runQuery()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form last" ng-if="current.useTags">
|
||||
<span class="gf-form-label width-10">Tags query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='current.tagsQuery' placeholder="metric name or tags query" ng-model-onblur></input>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="current.useTags">
|
||||
<li class="gf-form-label width-10">Tag values query</li>
|
||||
<input type="text" class="gf-form-input" ng-model='current.tagValuesQuery' placeholder="apps.$tag.*" ng-model-onblur></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-show="current.options.length">
|
||||
<h5>Preview of values (shows max 20)</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form" ng-repeat="option in current.options | limitTo: 20">
|
||||
<span class="gf-form-label">{{option.text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info gf-form-group" ng-if="infoText">
|
||||
{{infoText}}
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button type="submit" class="btn btn-success" ng-show="mode === 'edit'" ng-click="update();">Update</button>
|
||||
<button type="submit" class="btn btn-success" ng-show="mode === 'new'" ng-click="add();">Add</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-content-confirm-text" ng-if="confirmText">
|
||||
<input type="text" class="gf-form-input width-16" style="display: inline-block;" placeholder="Type {{confirmText}} to confirm" ng-model="confirmInput" ng-change="updateConfirmText(confirmInput)">
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
<dashnav dashboard="ctrl.dashboard"></dashnav>
|
||||
|
||||
<div class="scroll-canvas scroll-canvas--dashboard" grafana-scrollbar>
|
||||
<div dash-editor-view class="dash-edit-view"></div>
|
||||
<div class="dashboard-container">
|
||||
<dashboard-settings dashboard="ctrl.dashboard"
|
||||
ng-if="ctrl.dashboardViewState.state.editview"
|
||||
class="dashboard-settings">
|
||||
</dashboard-settings>
|
||||
|
||||
<div class="dashboard-container">
|
||||
<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
|
||||
</dashboard-submenu>
|
||||
|
||||
<dashboard-grid get-panel-container="ctrl.getPanelContainer">
|
||||
</dashboard-grid>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ function (angular, _, moment, dateMath, kbn, templatingVariable) {
|
||||
!!item.metricName &&
|
||||
!_.isEmpty(item.statistics);
|
||||
}).map(function (item) {
|
||||
item.region = templateSrv.replace(item.region, options.scopedVars);
|
||||
item.region = templateSrv.replace(self.getActualRegion(item.region), options.scopedVars);
|
||||
item.namespace = templateSrv.replace(item.namespace, options.scopedVars);
|
||||
item.metricName = templateSrv.replace(item.metricName, options.scopedVars);
|
||||
item.dimensions = self.convertDimensionFormat(item.dimensions, options.scopeVars);
|
||||
@@ -165,21 +165,21 @@ function (angular, _, moment, dateMath, kbn, templatingVariable) {
|
||||
|
||||
this.getMetrics = function (namespace, region) {
|
||||
return this.doMetricQueryRequest('metrics', {
|
||||
region: templateSrv.replace(region),
|
||||
region: templateSrv.replace(this.getActualRegion(region)),
|
||||
namespace: templateSrv.replace(namespace)
|
||||
});
|
||||
};
|
||||
|
||||
this.getDimensionKeys = function(namespace, region) {
|
||||
return this.doMetricQueryRequest('dimension_keys', {
|
||||
region: templateSrv.replace(region),
|
||||
region: templateSrv.replace(this.getActualRegion(region)),
|
||||
namespace: templateSrv.replace(namespace)
|
||||
});
|
||||
};
|
||||
|
||||
this.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) {
|
||||
return this.doMetricQueryRequest('dimension_values', {
|
||||
region: templateSrv.replace(region),
|
||||
region: templateSrv.replace(this.getActualRegion(region)),
|
||||
namespace: templateSrv.replace(namespace),
|
||||
metricName: templateSrv.replace(metricName),
|
||||
dimensionKey: templateSrv.replace(dimensionKey),
|
||||
@@ -189,14 +189,14 @@ function (angular, _, moment, dateMath, kbn, templatingVariable) {
|
||||
|
||||
this.getEbsVolumeIds = function(region, instanceId) {
|
||||
return this.doMetricQueryRequest('ebs_volume_ids', {
|
||||
region: templateSrv.replace(region),
|
||||
region: templateSrv.replace(this.getActualRegion(region)),
|
||||
instanceId: templateSrv.replace(instanceId)
|
||||
});
|
||||
};
|
||||
|
||||
this.getEc2InstanceAttribute = function(region, attributeName, filters) {
|
||||
return this.doMetricQueryRequest('ec2_instance_attribute', {
|
||||
region: templateSrv.replace(region),
|
||||
region: templateSrv.replace(this.getActualRegion(region)),
|
||||
attributeName: templateSrv.replace(attributeName),
|
||||
filters: filters
|
||||
});
|
||||
@@ -267,7 +267,7 @@ function (angular, _, moment, dateMath, kbn, templatingVariable) {
|
||||
period = parseInt(period, 10);
|
||||
var parameters = {
|
||||
prefixMatching: annotation.prefixMatching,
|
||||
region: templateSrv.replace(annotation.region),
|
||||
region: templateSrv.replace(this.getActualRegion(annotation.region)),
|
||||
namespace: templateSrv.replace(annotation.namespace),
|
||||
metricName: templateSrv.replace(annotation.metricName),
|
||||
dimensions: this.convertDimensionFormat(annotation.dimensions, {}),
|
||||
@@ -341,6 +341,13 @@ function (angular, _, moment, dateMath, kbn, templatingVariable) {
|
||||
return this.defaultRegion;
|
||||
};
|
||||
|
||||
this.getActualRegion = function(region) {
|
||||
if (region === 'default' || _.isEmpty(region)) {
|
||||
return this.getDefaultRegion();
|
||||
}
|
||||
return region;
|
||||
};
|
||||
|
||||
this.getExpandedVariables = function(target, dimensionKey, variable, templateSrv) {
|
||||
/* if the all checkbox is marked we should add all values to the targets */
|
||||
var allSelected = _.find(variable.options, {'selected': true, 'text': 'All'});
|
||||
|
||||
@@ -28,7 +28,7 @@ export class CloudWatchQueryParameterCtrl {
|
||||
target.statistics = target.statistics || ['Average'];
|
||||
target.dimensions = target.dimensions || {};
|
||||
target.period = target.period || '';
|
||||
target.region = target.region || '';
|
||||
target.region = target.region || 'default';
|
||||
|
||||
$scope.regionSegment = uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');
|
||||
$scope.namespaceSegment = uiSegmentSrv.getSegmentForValue($scope.target.namespace, 'select namespace');
|
||||
@@ -51,7 +51,7 @@ export class CloudWatchQueryParameterCtrl {
|
||||
$scope.removeStatSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove stat --'});
|
||||
|
||||
if (_.isEmpty($scope.target.region)) {
|
||||
$scope.target.region = $scope.datasource.getDefaultRegion();
|
||||
$scope.target.region = 'default';
|
||||
}
|
||||
|
||||
if (!$scope.onChange) {
|
||||
@@ -148,6 +148,10 @@ export class CloudWatchQueryParameterCtrl {
|
||||
|
||||
$scope.getRegions = function() {
|
||||
return $scope.datasource.metricFindQuery('regions()')
|
||||
.then(function(results) {
|
||||
results.unshift({ text: 'default'});
|
||||
return results;
|
||||
})
|
||||
.then($scope.transformToSegments(true));
|
||||
};
|
||||
|
||||
|
||||
@@ -165,6 +165,55 @@ describe('CloudWatchDatasource', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('When query region is "default"', function () {
|
||||
it('should return the datasource region if empty or "default"', function() {
|
||||
var defaultRegion = instanceSettings.jsonData.defaultRegion;
|
||||
|
||||
expect(ctx.ds.getActualRegion()).to.be(defaultRegion);
|
||||
expect(ctx.ds.getActualRegion('')).to.be(defaultRegion);
|
||||
expect(ctx.ds.getActualRegion("default")).to.be(defaultRegion);
|
||||
});
|
||||
|
||||
it('should return the specified region if specified', function() {
|
||||
expect(ctx.ds.getActualRegion('some-fake-region-1')).to.be('some-fake-region-1');
|
||||
});
|
||||
|
||||
var requestParams;
|
||||
beforeEach(function() {
|
||||
ctx.ds.performTimeSeriesQuery = function(request) {
|
||||
requestParams = request;
|
||||
return ctx.$q.when({data: {}});
|
||||
};
|
||||
});
|
||||
|
||||
it('should query for the datasource region if empty or "default"', function(done) {
|
||||
var query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
targets: [
|
||||
{
|
||||
region: 'default',
|
||||
namespace: 'AWS/EC2',
|
||||
metricName: 'CPUUtilization',
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678'
|
||||
},
|
||||
statistics: ['Average'],
|
||||
period: 300
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
ctx.ds.query(query).then(function(result) {
|
||||
expect(requestParams.queries[0].region).to.be(instanceSettings.jsonData.defaultRegion);
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('When performing CloudWatch query for extended statistics', function() {
|
||||
var query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
@@ -345,6 +394,26 @@ describe('CloudWatchDatasource', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('dimension_values(default,AWS/EC2,CPUUtilization,InstanceId)', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['i-12345678', 'i-12345678']] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __ListMetrics and return result', () => {
|
||||
expect(scenario.result[0].text).to.contain('i-12345678');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('dimension_values');
|
||||
});
|
||||
});
|
||||
|
||||
it('should caclculate the correct period', function () {
|
||||
var hourSec = 60 * 60;
|
||||
var daySec = hourSec * 24;
|
||||
|
||||
@@ -15,6 +15,10 @@ export class GraphiteConfigCtrl {
|
||||
}
|
||||
|
||||
autoDetectGraphiteVersion() {
|
||||
if (!this.current.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.datasourceSrv.loadDatasource(this.current.name)
|
||||
.then((ds) => {
|
||||
return ds.getVersion();
|
||||
|
||||
@@ -208,7 +208,7 @@ function (angular, _, $) {
|
||||
|
||||
if ($target.hasClass('fa-arrow-left')) {
|
||||
$scope.$apply(function() {
|
||||
_.move(ctrl.functions, $scope.$index, $scope.$index - 1);
|
||||
_.move(ctrl.queryModel.functions, $scope.$index, $scope.$index - 1);
|
||||
ctrl.targetChanged();
|
||||
});
|
||||
return;
|
||||
@@ -216,7 +216,7 @@ function (angular, _, $) {
|
||||
|
||||
if ($target.hasClass('fa-arrow-right')) {
|
||||
$scope.$apply(function() {
|
||||
_.move(ctrl.functions, $scope.$index, $scope.$index + 1);
|
||||
_.move(ctrl.queryModel.functions, $scope.$index, $scope.$index + 1);
|
||||
ctrl.targetChanged();
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class GraphiteQuery {
|
||||
}
|
||||
|
||||
try {
|
||||
this.parseTargetRecursive(astNode, null, 0);
|
||||
this.parseTargetRecursive(astNode, null);
|
||||
} catch (err) {
|
||||
console.log('error parsing target:', err.message);
|
||||
this.error = err.message;
|
||||
@@ -75,7 +75,7 @@ export default class GraphiteQuery {
|
||||
}, "");
|
||||
}
|
||||
|
||||
parseTargetRecursive(astNode, func, index) {
|
||||
parseTargetRecursive(astNode, func) {
|
||||
if (astNode === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -83,42 +83,35 @@ export default class GraphiteQuery {
|
||||
switch (astNode.type) {
|
||||
case 'function':
|
||||
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
|
||||
_.each(astNode.params, (param, index) => {
|
||||
this.parseTargetRecursive(param, innerFunc, index);
|
||||
_.each(astNode.params, param => {
|
||||
this.parseTargetRecursive(param, innerFunc);
|
||||
});
|
||||
|
||||
innerFunc.updateText();
|
||||
this.functions.push(innerFunc);
|
||||
break;
|
||||
case 'series-ref':
|
||||
this.addFunctionParameter(func, astNode.value, index, this.segments.length > 0);
|
||||
if (this.segments.length > 0) {
|
||||
this.addFunctionParameter(func, astNode.value);
|
||||
} else {
|
||||
this.segments.push(astNode);
|
||||
}
|
||||
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 };
|
||||
}
|
||||
var shiftBack = this.isShiftParamsBack(func);
|
||||
this.addFunctionParameter(func, astNode.value, index, shiftBack);
|
||||
break;
|
||||
this.addFunctionParameter(func, astNode.value);
|
||||
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, _.join(_.map(astNode.segments, 'value'), '.'));
|
||||
} else {
|
||||
this.segments = astNode.segments;
|
||||
}
|
||||
this.addFunctionParameter(func, astNode.segments[0].value, index, true);
|
||||
break;
|
||||
}
|
||||
|
||||
this.segments = astNode.segments;
|
||||
}
|
||||
}
|
||||
|
||||
isShiftParamsBack(func) {
|
||||
return func.def.name !== 'seriesByTag';
|
||||
}
|
||||
|
||||
updateSegmentValue(segment, index) {
|
||||
this.segments[index].value = segment.value;
|
||||
}
|
||||
@@ -127,6 +120,14 @@ export default class GraphiteQuery {
|
||||
this.segments.push({value: "select metric"});
|
||||
}
|
||||
|
||||
hasSelectMetric() {
|
||||
if (this.segments.length > 0) {
|
||||
return this.segments[this.segments.length - 1].value === 'select metric';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
addFunction(newFunc) {
|
||||
this.functions.push(newFunc);
|
||||
this.moveAliasFuncLast();
|
||||
@@ -145,11 +146,11 @@ export default class GraphiteQuery {
|
||||
}
|
||||
}
|
||||
|
||||
addFunctionParameter(func, value, index, shiftBack) {
|
||||
if (shiftBack) {
|
||||
index = Math.max(index - 1, 0);
|
||||
addFunctionParameter(func, value) {
|
||||
if (func.params.length >= func.def.params.length) {
|
||||
throw { message: 'too many parameters for function ' + func.def.name };
|
||||
}
|
||||
func.params[index] = value;
|
||||
func.params.push(value);
|
||||
}
|
||||
|
||||
removeFunction(func) {
|
||||
@@ -159,7 +160,7 @@ export default class GraphiteQuery {
|
||||
updateModelTarget(targets) {
|
||||
// render query
|
||||
if (!this.target.textEditor) {
|
||||
var metricPath = this.getSegmentPathUpTo(this.segments.length);
|
||||
var metricPath = this.getSegmentPathUpTo(this.segments.length).replace(/\.select metric$/, '');
|
||||
this.target.target = _.reduce(this.functions, wrapFunction, metricPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
|
||||
checkOtherSegments(fromIndex) {
|
||||
if (this.queryModel.segments.length === 1 && this.queryModel.segments[0].type === 'series-ref') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fromIndex === 0) {
|
||||
this.addSelectMetricSegment();
|
||||
return;
|
||||
@@ -108,8 +112,23 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
|
||||
if (altSegments.length === 0) { return altSegments; }
|
||||
|
||||
// add query references
|
||||
if (index === 0) {
|
||||
_.eachRight(this.panelCtrl.panel.targets, target => {
|
||||
if (target.refId === this.queryModel.target.refId) {
|
||||
return;
|
||||
}
|
||||
|
||||
altSegments.unshift(this.uiSegmentSrv.newSegment({
|
||||
type: 'series-ref',
|
||||
value: '#' + target.refId,
|
||||
expandable: false,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// add template variables
|
||||
_.each(this.templateSrv.variables, variable => {
|
||||
_.eachRight(this.templateSrv.variables, variable => {
|
||||
altSegments.unshift(this.uiSegmentSrv.newSegment({
|
||||
type: 'template',
|
||||
value: '$' + variable.name,
|
||||
@@ -199,11 +218,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
var oldTarget = this.queryModel.target.target;
|
||||
this.updateModelTarget();
|
||||
|
||||
if (this.queryModel.target !== oldTarget) {
|
||||
var lastSegment = this.segments.length > 0 ? this.segments[this.segments.length - 1] : {};
|
||||
if (lastSegment.value !== 'select metric') {
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
if (this.queryModel.target !== oldTarget && !this.queryModel.hasSelectMetric()) {
|
||||
this.panelCtrl.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,11 +95,11 @@ describe('GraphiteQueryCtrl', function() {
|
||||
});
|
||||
|
||||
it('should not add select metric segment', function() {
|
||||
expect(ctx.ctrl.segments.length).to.be(0);
|
||||
expect(ctx.ctrl.segments.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should add both series refs as params', function() {
|
||||
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(2);
|
||||
it('should add second series ref as param', function() {
|
||||
expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('GraphiteQueryCtrl', function() {
|
||||
|
||||
describe('when updating targets with nested query', function() {
|
||||
beforeEach(function() {
|
||||
ctx.ctrl.target.target = 'scaleToSeconds(#A)';
|
||||
ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
|
||||
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
|
||||
ctx.ctrl.parseTarget();
|
||||
|
||||
@@ -183,11 +183,11 @@ describe('GraphiteQueryCtrl', function() {
|
||||
});
|
||||
|
||||
it('target should remain the same', function() {
|
||||
expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A)');
|
||||
expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
|
||||
});
|
||||
|
||||
it('targetFull should include nexted queries', function() {
|
||||
expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count)');
|
||||
expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count, 60)');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -50,11 +50,11 @@ Macros:
|
||||
- $__timeEpoch -> extract(epoch from column) as "time"
|
||||
- $__timeFilter(column) -> extract(epoch from column) BETWEEN 1492750877 AND 1492750877
|
||||
- $__unixEpochFilter(column) -> column > 1492750877 AND column < 1492750877
|
||||
- $__timeGroup(column,'5m') -> (extract(epoch from "dateColumn")/300)::bigint*300
|
||||
- $__timeGroup(column,'5m') -> (extract(epoch from column)/300)::bigint*300 AS time
|
||||
|
||||
Example of group by and order by with $__timeGroup:
|
||||
SELECT
|
||||
$__timeGroup(date_time_col, '1h') AS time,
|
||||
$__timeGroup(date_time_col, '1h'),
|
||||
sum(value) as value
|
||||
FROM yourtable
|
||||
GROUP BY time
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import 'vendor/flot/jquery.flot';
|
||||
import 'vendor/flot/jquery.flot.selection';
|
||||
import 'vendor/flot/jquery.flot.time';
|
||||
|
||||
@@ -220,12 +220,32 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
|
||||
elem.append(tbodyElem);
|
||||
} else {
|
||||
elem.append(seriesElements);
|
||||
}
|
||||
|
||||
if (!legendScrollbar) {
|
||||
legendScrollbar = new PerfectScrollbar(elem[0]);
|
||||
} else {
|
||||
legendScrollbar.update();
|
||||
}
|
||||
if (!panel.legend.rightSide) {
|
||||
addScrollbar();
|
||||
} else {
|
||||
destroyScrollbar();
|
||||
}
|
||||
}
|
||||
|
||||
function addScrollbar() {
|
||||
const scrollbarOptions = {
|
||||
// Number of pixels the content height can surpass the container height without enabling the scroll bar.
|
||||
scrollYMarginOffset: 2,
|
||||
suppressScrollX: true
|
||||
};
|
||||
|
||||
if (!legendScrollbar) {
|
||||
legendScrollbar = new PerfectScrollbar(elem[0], scrollbarOptions);
|
||||
} else {
|
||||
legendScrollbar.update();
|
||||
}
|
||||
}
|
||||
|
||||
function destroyScrollbar() {
|
||||
if (legendScrollbar) {
|
||||
legendScrollbar.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li class="card-item-wrapper" ng-repeat="permission in ctrl.userGroupPermissions">
|
||||
<li class="card-item-wrapper" ng-repeat="permission in ctrl.teamPermissions">
|
||||
<div class="card-item card-item--alert">
|
||||
<div class="card-item-header">
|
||||
<div class="card-item-sub-name">{{permission.permissionName}}</div>
|
||||
</div>
|
||||
<div class="card-item-body">
|
||||
<div class="card-item-details">
|
||||
<div class="card-item-notice">{{permission.userGroup}}</div>
|
||||
<div class="card-item-notice">{{permission.team}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ class PermissionListCtrl extends PanelCtrl {
|
||||
static templateUrl = 'module.html';
|
||||
|
||||
userPermissions: any[];
|
||||
userGroupPermissions: any[];
|
||||
teamPermissions: any[];
|
||||
roles: any[];
|
||||
|
||||
panelDefaults = {
|
||||
@@ -48,7 +48,7 @@ class PermissionListCtrl extends PanelCtrl {
|
||||
return this.backendSrv.get(`/api/dashboards/id/${this.panel.folderId}/acl`)
|
||||
.then(result => {
|
||||
this.userPermissions = _.filter(result, p => { return p.userId > 0;});
|
||||
this.userGroupPermissions = _.filter(result, p => { return p.userGroupId > 0;});
|
||||
this.teamPermissions = _.filter(result, p => { return p.teamId > 0;});
|
||||
// this.roles = this.setRoles(result);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user