Merge branch 'develop' into 9879-login

This commit is contained in:
Johannes Schill
2017-12-13 09:22:48 +01:00
184 changed files with 3895 additions and 2362 deletions

View File

@@ -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">

View File

@@ -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"

View File

@@ -1,5 +1,3 @@
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
var template = `

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>&nbsp;
</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>

View File

@@ -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);

View File

@@ -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,

View File

@@ -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');
}
});
}
};
});

View File

@@ -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': {

View File

@@ -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);
}

View File

@@ -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,
})

View File

@@ -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);
}
});
}
}

View File

@@ -68,6 +68,13 @@ function popoverSrv($compile, $rootScope, $timeout) {
openDrop = drop;
openDrop.open();
}, 100);
// return close function
return function() {
if (drop) {
drop.close();
}
};
};
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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'">&gt; New</span>
<span ng-show="ctrl.mode === 'edit'">&gt; 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> &nbsp;
{{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> &nbsp;
<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>&nbsp;&nbsp;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 &amp; 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>

View File

@@ -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&#45;repeat="permission in ctrl.userGroupPermissions" class="permissionlist__item"> -->
<!-- <tr ng&#45;repeat="permission in ctrl.teamPermissions" class="permissionlist__item"> -->
<!-- <td><i class="fa fa&#45;fw fa&#45;users"></i></td> -->
<!-- <td>{{permission.userGroup}}</td> -->
<!-- <td>{{permission.team}}</td> -->
<!-- <td><select class="gf&#45;form&#45;input gf&#45;size&#45;auto" ng&#45;model="permission.permissions" ng&#45;options="p.value as p.text for p in ctrl.permissionTypeOptions" ng&#45;change="ctrl.updatePermission(permission)"></select></td> -->
<!-- <td class="text&#45;right"> -->
<!-- <a ng&#45;click="ctrl.removePermission(permission)" class="btn btn&#45;danger btn&#45;small"> -->

View File

@@ -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;

View File

@@ -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', () => {

View File

@@ -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';

View File

@@ -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() {

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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}]]);
});

View File

@@ -0,0 +1,4 @@
import { react2AngularDirective } from 'app/core/utils/react2angular';
import { DashboardGrid } from './DashboardGrid';
react2AngularDirective('dashboardGrid', DashboardGrid, [['getPanelContainer', {watchDepth: 'reference', wrapApply: false}]]);

View File

@@ -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>

View File

@@ -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');
}

View File

@@ -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'">
&gt; 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&hellip;</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>&nbsp;&nbsp;Restore
</a>
<a class="btn btn-outline-disabled btn-small" ng-show="revision.version === ctrl.dashboard.version">
<i class="fa fa-check"></i>&nbsp;&nbsp;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&hellip;</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>&nbsp;&nbsp;Restore
</a>
<a class="btn btn-outline-disabled btn-small" ng-show="revision.version === ctrl.dashboard.version">
<i class="fa fa-check"></i>&nbsp;&nbsp;Latest
</a>
</td>
</tr>
</tbody>
</table>
<div ng-if="ctrl.appending">
<i class="fa fa-spinner fa-spin"></i>
<em>Fetching more entries&hellip;</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>&nbsp;&nbsp;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&hellip;</em>
<em>Fetching more entries&hellip;</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>&nbsp;&nbsp;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>&nbsp;&nbsp;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&hellip;</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>&nbsp;&nbsp;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>

View File

@@ -1,5 +1,3 @@
///<reference path="../../../headers/common.d.ts" />
import './history_srv';
import _ from 'lodash';

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +1,3 @@
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
const template = `

View 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>

View 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);

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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';

View 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);

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}}

View File

@@ -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>

View File

@@ -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>
&nbsp;&nbsp;
<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>

View File

@@ -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');
});
});
});

View 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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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() {

View File

@@ -1,5 +1,3 @@
///<reference path="../../headers/common.d.ts" />
import _ from 'lodash';
import config from 'app/core/config';

View File

@@ -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);

View File

@@ -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'">&gt; New</span>
<span ng-show="mode === 'edit'">&gt; 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>&nbsp;&nbsp;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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'});

View File

@@ -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));
};

View File

@@ -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;

View File

@@ -15,6 +15,10 @@ export class GraphiteConfigCtrl {
}
autoDetectGraphiteVersion() {
if (!this.current.id) {
return;
}
this.datasourceSrv.loadDatasource(this.current.name)
.then((ds) => {
return ds.getVersion();

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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)');
});
});

View File

@@ -50,11 +50,11 @@ Macros:
- $__timeEpoch -&gt; extract(epoch from column) as "time"
- $__timeFilter(column) -&gt; extract(epoch from column) BETWEEN 1492750877 AND 1492750877
- $__unixEpochFilter(column) -&gt; column &gt; 1492750877 AND column &lt; 1492750877
- $__timeGroup(column,'5m') -&gt; (extract(epoch from "dateColumn")/300)::bigint*300
- $__timeGroup(column,'5m') -&gt; (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

View File

@@ -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';

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -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);
});
}