mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into mssql_datasource
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import { QueryPartDef, QueryPart } from 'app/core/components/query_part/query_part';
|
||||
|
||||
@@ -140,10 +138,6 @@ function getAlertAnnotationInfo(ah) {
|
||||
return 'Error: ' + ah.data.error;
|
||||
}
|
||||
|
||||
if (ah.data.noData || ah.data.no_data) {
|
||||
return 'No Data';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { coreModule, appEvents } from 'app/core/core';
|
||||
import alertDef from './alert_def';
|
||||
|
||||
export class AlertListCtrl {
|
||||
alerts: any;
|
||||
stateFilters = [
|
||||
{ text: 'All', value: null },
|
||||
{ text: 'OK', value: 'ok' },
|
||||
{ text: 'Not OK', value: 'not_ok' },
|
||||
{ text: 'Alerting', value: 'alerting' },
|
||||
{ text: 'No Data', value: 'no_data' },
|
||||
{ text: 'Paused', value: 'paused' },
|
||||
];
|
||||
filters = {
|
||||
state: 'ALL',
|
||||
};
|
||||
navModel: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $location, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('alerting', 'alert-list', 0);
|
||||
|
||||
var params = $location.search();
|
||||
this.filters.state = params.state || null;
|
||||
this.loadAlerts();
|
||||
}
|
||||
|
||||
filtersChanged() {
|
||||
this.$location.search(this.filters);
|
||||
}
|
||||
|
||||
loadAlerts() {
|
||||
this.backendSrv.get('/api/alerts', this.filters).then(result => {
|
||||
this.alerts = _.map(result, alert => {
|
||||
alert.stateModel = alertDef.getStateDisplayModel(alert.state);
|
||||
alert.newStateDateAgo = moment(alert.newStateDate)
|
||||
.fromNow()
|
||||
.replace(' ago', '');
|
||||
if (alert.evalData && alert.evalData.no_data) {
|
||||
alert.no_data = true;
|
||||
}
|
||||
return alert;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pauseAlertRule(alertId: any) {
|
||||
var alert = _.find(this.alerts, { id: alertId });
|
||||
|
||||
var payload = {
|
||||
paused: alert.state !== 'paused',
|
||||
};
|
||||
|
||||
this.backendSrv.post(`/api/alerts/${alert.id}/pause`, payload).then(result => {
|
||||
alert.state = result.state;
|
||||
alert.stateModel = alertDef.getStateDisplayModel(result.state);
|
||||
});
|
||||
}
|
||||
|
||||
openHowTo() {
|
||||
appEvents.emit('show-modal', {
|
||||
src: 'public/app/features/alerting/partials/alert_howto.html',
|
||||
modalClass: 'confirm-modal',
|
||||
model: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('AlertListCtrl', AlertListCtrl);
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import { ThresholdMapper } from './threshold_mapper';
|
||||
import { QueryPart } from 'app/core/components/query_part/query_part';
|
||||
@@ -77,7 +75,7 @@ export class AlertTabCtrl {
|
||||
|
||||
getAlertHistory() {
|
||||
this.backendSrv
|
||||
.get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50`)
|
||||
.get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50&type=alert`)
|
||||
.then(res => {
|
||||
this.alertHistory = _.map(res, ah => {
|
||||
ah.time = this.dashboardSrv.getCurrent().formatDate(ah.time, 'MMM D, YYYY HH:mm:ss');
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
import './alert_list_ctrl';
|
||||
import './notifications_list_ctrl';
|
||||
import './notification_edit_ctrl';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import { appEvents, coreModule } from 'app/core/core';
|
||||
|
||||
@@ -60,15 +58,29 @@ export class AlertNotificationEditCtrl {
|
||||
}
|
||||
|
||||
if (this.model.id) {
|
||||
this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => {
|
||||
this.model = res;
|
||||
appEvents.emit('alert-success', ['Notification updated', '']);
|
||||
});
|
||||
this.backendSrv
|
||||
.put(`/api/alert-notifications/${this.model.id}`, this.model)
|
||||
.then(res => {
|
||||
this.model = res;
|
||||
appEvents.emit('alert-success', ['Notification updated', '']);
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.data && err.data.error) {
|
||||
appEvents.emit('alert-error', [err.data.error]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => {
|
||||
appEvents.emit('alert-success', ['Notification created', '']);
|
||||
this.$location.path('alerting/notifications');
|
||||
});
|
||||
this.backendSrv
|
||||
.post(`/api/alert-notifications`, this.model)
|
||||
.then(res => {
|
||||
appEvents.emit('alert-success', ['Notification created', '']);
|
||||
this.$location.path('alerting/notifications');
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.data && err.data.error) {
|
||||
appEvents.emit('alert-error', [err.data.error]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import { coreModule } from 'app/core/core';
|
||||
|
||||
export class AlertNotificationsListCtrl {
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
|
||||
<div class="page-action-bar">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">Filter by state</label>
|
||||
<div class="gf-form-select-wrapper width-13">
|
||||
<select class="gf-form-input" ng-model="ctrl.filters.state" ng-options="f.value as f.text for f in ctrl.stateFilters" ng-change="ctrl.filtersChanged()">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-action-bar__spacer">
|
||||
</div>
|
||||
|
||||
<a class="btn btn-secondary" ng-click="ctrl.openHowTo()">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
How to add an alert
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<section class="card-section card-list-layout-list">
|
||||
|
||||
<ol class="card-list" >
|
||||
<li class="card-item-wrapper" ng-repeat="alert in ctrl.alerts">
|
||||
<div class="card-item card-item--alert">
|
||||
<div class="card-item-header">
|
||||
<div class="card-item-type">
|
||||
<a class="card-item-cog" bs-tooltip="'Pausing an alert rule prevents it from executing'" ng-click="ctrl.pauseAlertRule(alert.id)">
|
||||
<i ng-show="alert.state !== 'paused'" class="fa fa-pause"></i>
|
||||
<i ng-show="alert.state === 'paused'" class="fa fa-play"></i>
|
||||
</a>
|
||||
<a class="card-item-cog" href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert" bs-tooltip="'Edit alert rule'">
|
||||
<i class="icon-gf icon-gf-settings"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-item-body">
|
||||
<div class="card-item-details">
|
||||
<div class="card-item-name">
|
||||
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
|
||||
{{alert.name}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-item-sub-name">
|
||||
<span class="alert-list-item-state {{alert.stateModel.stateClass}}">
|
||||
<i class="{{alert.stateModel.iconClass}}"></i>
|
||||
{{alert.stateModel.text}} <span class="small muted" ng-show="alert.no_data">(due to no data)</span>
|
||||
</span> for {{alert.newStateDateAgo}}
|
||||
</div>
|
||||
<div class="small muted" ng-show="alert.executionError !== ''">
|
||||
Error: "{{alert.executionError}}"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
@@ -12,7 +12,7 @@
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||
</li>
|
||||
<li>
|
||||
<li>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -143,36 +143,33 @@
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
|
||||
<section class="card-section card-list-layout-list">
|
||||
<ol class="card-list" >
|
||||
<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">
|
||||
<div class="alert-list card-item card-item--alert">
|
||||
<div class="alert-list-body">
|
||||
<div class="alert-list-icon alert-list-item-state {{ah.stateModel.stateClass}}">
|
||||
<i class="{{ah.stateModel.iconClass}}"></i>
|
||||
</div>
|
||||
<div class="alert-list-main alert-list-text">
|
||||
<span class="alert-list-state {{ah.stateModel.stateClass}}">{{ah.stateModel.text}}</span>
|
||||
<span class="alert-list-info">{{ah.info}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert-list-footer alert-list-text">
|
||||
<span>{{ah.time}}</span>
|
||||
<span><!--Img Link--></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="alert-rule-list" >
|
||||
<li class="alert-rule-item" ng-repeat="al in ctrl.alertHistory">
|
||||
<div class="alert-rule-item__icon {{al.stateModel.stateClass}}">
|
||||
<i class="{{al.stateModel.iconClass}}"></i>
|
||||
</div>
|
||||
<div class="alert-rule-item__body">
|
||||
<div class="alert-rule-item__header">
|
||||
<div class="alert-rule-item__text">
|
||||
<span class="{{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="alert-list-info">{{al.info}}</span>
|
||||
</div>
|
||||
<div class="alert-rule-item__time">
|
||||
<span>{{al.time}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="!ctrl.alert">
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.enable()">
|
||||
<i class="icon-gf icon-gf-alert"></i>
|
||||
Create Alert
|
||||
</button>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.enable()">
|
||||
<i class="icon-gf icon-gf-alert"></i>
|
||||
Create Alert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<div ng-if="ctrl.notifications.length === 0">
|
||||
<empty-list-cta model="{
|
||||
title: 'There are no notification channels defined yet',
|
||||
buttonIcon: 'gicon gicon-alert-notification-channel',
|
||||
buttonIcon: 'gicon gicon-add-alert-notification-channel',
|
||||
buttonLink: 'alerting/notification/new',
|
||||
buttonTitle: 'Add channel',
|
||||
proTip: 'You can include images in your alert notifications.',
|
||||
|
||||
@@ -8,5 +8,8 @@ define([
|
||||
'./playlist/all',
|
||||
'./snapshot/all',
|
||||
'./panel/all',
|
||||
'./org/all',
|
||||
'./admin/admin',
|
||||
'./alerting/all',
|
||||
'./styleguide/styleguide',
|
||||
], function () {});
|
||||
|
||||
@@ -54,7 +54,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
|
||||
`;
|
||||
|
||||
// Show edit icon only for users with at least Editor role
|
||||
if (event.id && contextSrv.isEditor) {
|
||||
if (event.id && dashboard.meta.canEdit) {
|
||||
header += `
|
||||
<span class="pointer graph-annotation__edit-icon" ng-click="onEdit()">
|
||||
<i class="fa fa-pencil-square"></i>
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
<div class="modal-body">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
<i class="fa fa-lock"></i>
|
||||
<span class="p-l-1">Permissions</span>
|
||||
</h2>
|
||||
|
||||
<a class="modal-header-close" ng-click="ctrl.dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<table class="filter-table gf-form-group">
|
||||
<tr ng-repeat="acl in ctrl.items" ng-class="{'gf-form-disabled': acl.inherited}">
|
||||
<td style="width: 100%;">
|
||||
<i class="{{acl.icon}}"></i>
|
||||
<span ng-bind-html="acl.nameHtml"></span>
|
||||
</td>
|
||||
<td>
|
||||
<em class="muted no-wrap" ng-show="acl.inherited">Inherited from folder</em>
|
||||
</td>
|
||||
<td class="query-keyword">Can</td>
|
||||
<td>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="acl.permission" ng-options="p.value as p.text for p in ctrl.permissionOptions" ng-change="ctrl.permissionChanged(acl)" ng-disabled="acl.inherited"></select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-inverse btn-small" ng-click="ctrl.removeItem($index)" ng-hide="acl.inherited">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="ctrl.aclItems.length === 0">
|
||||
<td colspan="4">
|
||||
<em>No permissions. Will only be accessible by admins.</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<form name="addPermission" class="gf-form-group">
|
||||
<h6 class="muted">Add Permission For</h6>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.newType" ng-options="p.value as p.text for p in ctrl.aclTypes" ng-change="ctrl.typeChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.newType === 'User'">
|
||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.newType === 'Group'">
|
||||
<team-picker team-picked="ctrl.groupPicked($group)"></team-picker>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="gf-form width-17">
|
||||
<span ng-if="ctrl.error" class="text-error p-l-1">
|
||||
<i class="fa fa-warning"></i>
|
||||
{{ctrl.error}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row text-center">
|
||||
<button type="button" class="btn btn-danger" ng-disabled="!ctrl.canUpdate" ng-click="ctrl.update()">
|
||||
Update Permissions
|
||||
</button>
|
||||
<a class="btn-text" ng-click="ctrl.dismiss();">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <br> -->
|
||||
<!-- <br> -->
|
||||
<!-- <br> -->
|
||||
<!-- -->
|
||||
<!-- <div class="permissionlist"> -->
|
||||
<!-- <div class="permissionlist__section"> -->
|
||||
<!-- <div class="permissionlist__section-header"> -->
|
||||
<!-- <h6>Permissions</h6> -->
|
||||
<!-- </div> -->
|
||||
<!-- <table class="filter-table form-inline"> -->
|
||||
<!-- <thead> -->
|
||||
<!-- <tr> -->
|
||||
<!-- <th style="width: 50px;"></th> -->
|
||||
<!-- <th>Name</th> -->
|
||||
<!-- <th style="width: 220px;">Permission</th> -->
|
||||
<!-- <th style="width: 120px"></th> -->
|
||||
<!-- </tr> -->
|
||||
<!-- </thead> -->
|
||||
<!-- <tbody> -->
|
||||
<!-- <tr ng-repeat="permission in ctrl.userPermissions" class="permissionlist__item"> -->
|
||||
<!-- <td><i class="fa fa-fw fa-user"></i></td> -->
|
||||
<!-- <td>{{permission.userLogin}}</td> -->
|
||||
<!-- <td class="text-right"> -->
|
||||
<!-- <a ng-click="ctrl.removePermission(permission)" class="btn btn-danger btn-small"> -->
|
||||
<!-- <i class="fa fa-remove"></i> -->
|
||||
<!-- </a> -->
|
||||
<!-- </td> -->
|
||||
<!-- </tr> -->
|
||||
<!-- <tr ng-repeat="permission in ctrl.teamPermissions" class="permissionlist__item"> -->
|
||||
<!-- <td><i class="fa fa-fw fa-users"></i></td> -->
|
||||
<!-- <td>{{permission.team}}</td> -->
|
||||
<!-- <td><select class="gf-form-input gf-size-auto" ng-model="permission.permissions" ng-options="p.value as p.text for p in ctrl.permissionTypeOptions" ng-change="ctrl.updatePermission(permission)"></select></td> -->
|
||||
<!-- <td class="text-right"> -->
|
||||
<!-- <a ng-click="ctrl.removePermission(permission)" class="btn btn-danger btn-small"> -->
|
||||
<!-- <i class="fa fa-remove"></i> -->
|
||||
<!-- </a> -->
|
||||
<!-- </td> -->
|
||||
<!-- </tr> -->
|
||||
<!-- <tr ng-repeat="role in ctrl.roles" class="permissionlist__item"> -->
|
||||
<!-- <td></td> -->
|
||||
<!-- <td>{{role.name}}</td> -->
|
||||
<!-- <td><select class="gf-form-input gf-size-auto" ng-model="role.permissions" ng-options="p.value as p.text for p in ctrl.roleOptions" ng-change="ctrl.updatePermission(role)"></select></td> -->
|
||||
<!-- <td class="text-right"> -->
|
||||
<!-- -->
|
||||
<!-- </td> -->
|
||||
<!-- </tr> -->
|
||||
<!-- </tbody> -->
|
||||
<!-- </table> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
@@ -1,203 +0,0 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class AclCtrl {
|
||||
dashboard: any;
|
||||
items: DashboardAcl[];
|
||||
permissionOptions = [{ value: 1, text: 'View' }, { value: 2, text: 'Edit' }, { value: 4, text: 'Admin' }];
|
||||
aclTypes = [
|
||||
{ value: 'Group', text: 'Team' },
|
||||
{ value: 'User', text: 'User' },
|
||||
{ value: 'Viewer', text: 'Everyone With Viewer Role' },
|
||||
{ value: 'Editor', text: 'Everyone With Editor Role' },
|
||||
];
|
||||
|
||||
dismiss: () => void;
|
||||
newType: string;
|
||||
canUpdate: boolean;
|
||||
error: string;
|
||||
|
||||
readonly duplicateError = 'This permission exists already.';
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, dashboardSrv, private $sce, private $scope) {
|
||||
this.items = [];
|
||||
this.resetNewType();
|
||||
this.dashboard = dashboardSrv.getCurrent();
|
||||
this.get(this.dashboard.id);
|
||||
}
|
||||
|
||||
resetNewType() {
|
||||
this.newType = 'Group';
|
||||
}
|
||||
|
||||
get(dashboardId: number) {
|
||||
return this.backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`).then(result => {
|
||||
this.items = _.map(result, this.prepareViewModel.bind(this));
|
||||
this.sortItems();
|
||||
});
|
||||
}
|
||||
|
||||
sortItems() {
|
||||
this.items = _.orderBy(this.items, ['sortRank', 'sortName'], ['desc', 'asc']);
|
||||
}
|
||||
|
||||
prepareViewModel(item: DashboardAcl): DashboardAcl {
|
||||
item.inherited = !this.dashboard.meta.isFolder && this.dashboard.id !== item.dashboardId;
|
||||
item.sortRank = 0;
|
||||
|
||||
if (item.userId > 0) {
|
||||
item.icon = 'fa fa-fw fa-user';
|
||||
item.nameHtml = this.$sce.trustAsHtml(item.userLogin);
|
||||
item.sortName = item.userLogin;
|
||||
item.sortRank = 10;
|
||||
} else if (item.teamId > 0) {
|
||||
item.icon = 'fa fa-fw fa-users';
|
||||
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';
|
||||
item.nameHtml = this.$sce.trustAsHtml(`Everyone with <span class="query-keyword">${item.role}</span> Role`);
|
||||
item.sortName = item.role;
|
||||
item.sortRank = 30;
|
||||
if (item.role === 'Viewer') {
|
||||
item.sortRank += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.inherited) {
|
||||
item.sortRank += 100;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
update() {
|
||||
var updated = [];
|
||||
for (let item of this.items) {
|
||||
if (item.inherited) {
|
||||
continue;
|
||||
}
|
||||
updated.push({
|
||||
id: item.id,
|
||||
userId: item.userId,
|
||||
teamId: item.teamId,
|
||||
role: item.role,
|
||||
permission: item.permission,
|
||||
});
|
||||
}
|
||||
|
||||
return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, { items: updated }).then(() => {
|
||||
return this.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
typeChanged() {
|
||||
if (this.newType === 'Viewer' || this.newType === 'Editor') {
|
||||
this.addNewItem({ permission: 1, role: this.newType });
|
||||
this.canUpdate = true;
|
||||
this.resetNewType();
|
||||
}
|
||||
}
|
||||
|
||||
permissionChanged() {
|
||||
this.canUpdate = true;
|
||||
}
|
||||
|
||||
addNewItem(item) {
|
||||
if (!this.isValid(item)) {
|
||||
return;
|
||||
}
|
||||
this.error = '';
|
||||
|
||||
item.dashboardId = this.dashboard.id;
|
||||
|
||||
this.items.push(this.prepareViewModel(item));
|
||||
this.sortItems();
|
||||
|
||||
this.canUpdate = true;
|
||||
}
|
||||
|
||||
isValid(item) {
|
||||
const dupe = _.find(this.items, it => {
|
||||
return this.isDuplicate(it, item);
|
||||
});
|
||||
|
||||
if (dupe) {
|
||||
this.error = this.duplicateError;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
isDuplicate(origItem, newItem) {
|
||||
if (origItem.inherited) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(origItem.role && newItem.role && origItem.role === newItem.role) ||
|
||||
(origItem.userId && newItem.userId && origItem.userId === newItem.userId) ||
|
||||
(origItem.teamId && newItem.teamId && origItem.teamId === newItem.teamId)
|
||||
);
|
||||
}
|
||||
|
||||
userPicked(user) {
|
||||
this.addNewItem({ userId: user.id, userLogin: user.login, permission: 1 });
|
||||
this.$scope.$broadcast('user-picker-reset');
|
||||
}
|
||||
|
||||
groupPicked(group) {
|
||||
this.addNewItem({ teamId: group.id, team: group.name, permission: 1 });
|
||||
this.$scope.$broadcast('team-picker-reset');
|
||||
}
|
||||
|
||||
removeItem(index) {
|
||||
this.items.splice(index, 1);
|
||||
this.canUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function dashAclModal() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/acl/acl.html',
|
||||
controller: AclCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
dismiss: '&',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface FormModel {
|
||||
dashboardId: number;
|
||||
userId?: number;
|
||||
teamId?: number;
|
||||
PermissionType: number;
|
||||
}
|
||||
|
||||
export interface DashboardAcl {
|
||||
id?: number;
|
||||
dashboardId?: number;
|
||||
userId?: number;
|
||||
userLogin?: string;
|
||||
userEmail?: string;
|
||||
teamId?: number;
|
||||
team?: string;
|
||||
permission?: number;
|
||||
permissionName?: string;
|
||||
role?: string;
|
||||
icon?: string;
|
||||
nameHtml?: string;
|
||||
inherited?: boolean;
|
||||
sortName?: string;
|
||||
sortRank?: number;
|
||||
}
|
||||
|
||||
coreModule.directive('dashAclModal', dashAclModal);
|
||||
@@ -1,188 +0,0 @@
|
||||
import { describe, beforeEach, it, expect, sinon, angularMocks } from 'test/lib/common';
|
||||
import { AclCtrl } from '../acl';
|
||||
|
||||
describe('AclCtrl', () => {
|
||||
const ctx: any = {};
|
||||
const backendSrv = {
|
||||
get: sinon.stub().returns(Promise.resolve([])),
|
||||
post: sinon.stub().returns(Promise.resolve([])),
|
||||
};
|
||||
|
||||
const dashboardSrv = {
|
||||
getCurrent: sinon.stub().returns({ id: 1, meta: { isFolder: false } }),
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.controllers'));
|
||||
|
||||
beforeEach(
|
||||
angularMocks.inject(($rootScope, $controller, $q, $compile) => {
|
||||
ctx.$q = $q;
|
||||
ctx.scope = $rootScope.$new();
|
||||
AclCtrl.prototype.dashboard = { dashboard: { id: 1 } };
|
||||
ctx.ctrl = $controller(
|
||||
AclCtrl,
|
||||
{
|
||||
$scope: ctx.scope,
|
||||
backendSrv: backendSrv,
|
||||
dashboardSrv: dashboardSrv,
|
||||
},
|
||||
{
|
||||
dismiss: () => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
describe('when permissions are added', () => {
|
||||
beforeEach(() => {
|
||||
backendSrv.get.reset();
|
||||
backendSrv.post.reset();
|
||||
|
||||
const userItem = {
|
||||
id: 2,
|
||||
login: 'user2',
|
||||
};
|
||||
|
||||
ctx.ctrl.userPicked(userItem);
|
||||
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: 'ug1',
|
||||
};
|
||||
|
||||
ctx.ctrl.groupPicked(teamItem);
|
||||
|
||||
ctx.ctrl.newType = 'Editor';
|
||||
ctx.ctrl.typeChanged();
|
||||
|
||||
ctx.ctrl.newType = 'Viewer';
|
||||
ctx.ctrl.typeChanged();
|
||||
});
|
||||
|
||||
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].teamId).to.eql(2);
|
||||
expect(ctx.ctrl.items[3].userId).to.eql(2);
|
||||
});
|
||||
|
||||
it('should save permissions to db', done => {
|
||||
ctx.ctrl.update().then(() => {
|
||||
done();
|
||||
});
|
||||
|
||||
expect(backendSrv.post.getCall(0).args[0]).to.eql('/api/dashboards/id/1/acl');
|
||||
expect(backendSrv.post.getCall(0).args[1].items[0].role).to.eql('Viewer');
|
||||
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].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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when duplicate role permissions are added', () => {
|
||||
beforeEach(() => {
|
||||
backendSrv.get.reset();
|
||||
backendSrv.post.reset();
|
||||
ctx.ctrl.items = [];
|
||||
|
||||
ctx.ctrl.newType = 'Editor';
|
||||
ctx.ctrl.typeChanged();
|
||||
|
||||
ctx.ctrl.newType = 'Editor';
|
||||
ctx.ctrl.typeChanged();
|
||||
});
|
||||
|
||||
it('should throw a validation error', () => {
|
||||
expect(ctx.ctrl.error).to.eql(ctx.ctrl.duplicateError);
|
||||
});
|
||||
|
||||
it('should not add the duplicate permission', () => {
|
||||
expect(ctx.ctrl.items.length).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when duplicate user permissions are added', () => {
|
||||
beforeEach(() => {
|
||||
backendSrv.get.reset();
|
||||
backendSrv.post.reset();
|
||||
ctx.ctrl.items = [];
|
||||
|
||||
const userItem = {
|
||||
id: 2,
|
||||
login: 'user2',
|
||||
};
|
||||
|
||||
ctx.ctrl.userPicked(userItem);
|
||||
ctx.ctrl.userPicked(userItem);
|
||||
});
|
||||
|
||||
it('should throw a validation error', () => {
|
||||
expect(ctx.ctrl.error).to.eql(ctx.ctrl.duplicateError);
|
||||
});
|
||||
|
||||
it('should not add the duplicate permission', () => {
|
||||
expect(ctx.ctrl.items.length).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when duplicate team permissions are added', () => {
|
||||
beforeEach(() => {
|
||||
backendSrv.get.reset();
|
||||
backendSrv.post.reset();
|
||||
ctx.ctrl.items = [];
|
||||
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: 'ug1',
|
||||
};
|
||||
|
||||
ctx.ctrl.groupPicked(teamItem);
|
||||
ctx.ctrl.groupPicked(teamItem);
|
||||
});
|
||||
|
||||
it('should throw a validation error', () => {
|
||||
expect(ctx.ctrl.error).to.eql(ctx.ctrl.duplicateError);
|
||||
});
|
||||
|
||||
it('should not add the duplicate permission', () => {
|
||||
expect(ctx.ctrl.items.length).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when one inherited and one not inherited team permission are added', () => {
|
||||
beforeEach(() => {
|
||||
backendSrv.get.reset();
|
||||
backendSrv.post.reset();
|
||||
ctx.ctrl.items = [];
|
||||
|
||||
const inheritedTeamItem = {
|
||||
id: 2,
|
||||
name: 'ug1',
|
||||
dashboardId: -1,
|
||||
};
|
||||
|
||||
ctx.ctrl.items.push(inheritedTeamItem);
|
||||
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: 'ug1',
|
||||
};
|
||||
ctx.ctrl.groupPicked(teamItem);
|
||||
});
|
||||
|
||||
it('should not throw a validation error', () => {
|
||||
expect(ctx.ctrl.error).to.eql('');
|
||||
});
|
||||
|
||||
it('should add both permissions', () => {
|
||||
expect(ctx.ctrl.items.length).to.eql(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export class AlertingSrv {
|
||||
|
||||
@@ -23,7 +23,6 @@ import './repeat_option/repeat_option';
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
import './dashgrid/PanelLoader';
|
||||
import './dashgrid/RowOptions';
|
||||
import './acl/acl';
|
||||
import './folder_picker/folder_picker';
|
||||
import './move_to_folder_modal/move_to_folder';
|
||||
import './settings/settings';
|
||||
@@ -31,14 +30,12 @@ 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';
|
||||
import { FolderSettingsCtrl } from './folder_settings_ctrl';
|
||||
import { DashboardImportCtrl } from './dashboard_import_ctrl';
|
||||
import { CreateFolderCtrl } from './create_folder_ctrl';
|
||||
|
||||
coreModule.controller('DashboardListCtrl', DashboardListCtrl);
|
||||
coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl);
|
||||
coreModule.controller('FolderPermissionsCtrl', FolderPermissionsCtrl);
|
||||
coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl);
|
||||
coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);
|
||||
coreModule.controller('CreateFolderCtrl', CreateFolderCtrl);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import appEvents from 'app/core/app_events';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
export class CreateFolderCtrl {
|
||||
title = '';
|
||||
@@ -17,11 +18,9 @@ export class CreateFolderCtrl {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.backendSrv.createDashboardFolder(this.title).then(result => {
|
||||
return this.backendSrv.createFolder({ title: this.title }).then(result => {
|
||||
appEvents.emit('alert-success', ['Folder Created', 'OK']);
|
||||
|
||||
var folderUrl = `dashboards/folder/${result.dashboard.id}/${result.meta.slug}`;
|
||||
this.$location.url(folderUrl);
|
||||
this.$location.url(locationUtil.stripBaseFromUrl(result.url));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +28,7 @@ export class CreateFolderCtrl {
|
||||
this.titleTouched = true;
|
||||
|
||||
this.validationSrv
|
||||
.validateNewDashboardOrFolderName(this.title)
|
||||
.validateNewFolderName(this.title)
|
||||
.then(() => {
|
||||
this.hasValidationError = false;
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import config from 'app/core/config';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { PanelContainer } from './dashgrid/PanelContainer';
|
||||
import { DashboardModel } from './dashboard_model';
|
||||
import { PanelModel } from './panel_model';
|
||||
|
||||
export class DashboardCtrl implements PanelContainer {
|
||||
dashboard: DashboardModel;
|
||||
@@ -130,9 +131,47 @@ export class DashboardCtrl implements PanelContainer {
|
||||
return this;
|
||||
}
|
||||
|
||||
onRemovingPanel(evt, options) {
|
||||
options = options || {};
|
||||
if (!options.panelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
var panelInfo = this.dashboard.getPanelInfoById(options.panelId);
|
||||
this.removePanel(panelInfo.panel, true);
|
||||
}
|
||||
|
||||
removePanel(panel: PanelModel, ask: boolean) {
|
||||
// confirm deletion
|
||||
if (ask !== false) {
|
||||
var text2, confirmText;
|
||||
|
||||
if (panel.alert) {
|
||||
text2 = 'Panel includes an alert rule, removing panel will also remove alert rule';
|
||||
confirmText = 'YES';
|
||||
}
|
||||
|
||||
this.$scope.appEvent('confirm-modal', {
|
||||
title: 'Remove Panel',
|
||||
text: 'Are you sure you want to remove this panel?',
|
||||
text2: text2,
|
||||
icon: 'fa-trash',
|
||||
confirmText: confirmText,
|
||||
yesText: 'Remove',
|
||||
onConfirm: () => {
|
||||
this.removePanel(panel, false);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.dashboard.removePanel(panel);
|
||||
}
|
||||
|
||||
init(dashboard) {
|
||||
this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
|
||||
this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
|
||||
this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
|
||||
this.setupDashboard(dashboard);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class DashboardImportCtrl {
|
||||
nameValidationError: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private validationSrv, navModelSrv, private $location, private $scope, $routeParams) {
|
||||
constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) {
|
||||
this.navModel = navModelSrv.getNav('create', 'import');
|
||||
|
||||
this.step = 1;
|
||||
@@ -93,7 +93,7 @@ export class DashboardImportCtrl {
|
||||
this.nameExists = false;
|
||||
|
||||
this.validationSrv
|
||||
.validateNewDashboardOrFolderName(this.dash.title)
|
||||
.validateNewDashboardName(0, this.dash.title)
|
||||
.then(() => {
|
||||
this.hasNameValidationError = false;
|
||||
})
|
||||
@@ -124,8 +124,7 @@ export class DashboardImportCtrl {
|
||||
inputs: inputs,
|
||||
})
|
||||
.then(res => {
|
||||
this.$location.url('dashboard/' + res.importedUri);
|
||||
this.$scope.dismiss();
|
||||
this.$location.url(res.importedUrl);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -35,18 +35,18 @@ export class DashboardLoaderSrv {
|
||||
};
|
||||
}
|
||||
|
||||
loadDashboard(type, slug) {
|
||||
loadDashboard(type, slug, uid) {
|
||||
var promise;
|
||||
|
||||
if (type === 'script') {
|
||||
promise = this._loadScriptedDashboard(slug);
|
||||
} else if (type === 'snapshot') {
|
||||
promise = this.backendSrv.get('/api/snapshots/' + this.$routeParams.slug).catch(() => {
|
||||
promise = this.backendSrv.get('/api/snapshots/' + slug).catch(() => {
|
||||
return this._dashboardLoadFailed('Snapshot not found', true);
|
||||
});
|
||||
} else {
|
||||
promise = this.backendSrv
|
||||
.getDashboard(this.$routeParams.type, this.$routeParams.slug)
|
||||
.getDashboardByUid(uid)
|
||||
.then(result => {
|
||||
if (result.meta.isFolder) {
|
||||
this.$rootScope.appEvent('alert-error', ['Dashboard not found']);
|
||||
|
||||
@@ -18,7 +18,7 @@ export class DashboardMigrator {
|
||||
}
|
||||
|
||||
updateSchema(old) {
|
||||
var i, j, k;
|
||||
var i, j, k, n;
|
||||
var oldVersion = this.dashboard.schemaVersion;
|
||||
var panelUpgrades = [];
|
||||
this.dashboard.schemaVersion = 16;
|
||||
@@ -63,11 +63,17 @@ export class DashboardMigrator {
|
||||
}
|
||||
|
||||
if (panel.y_format) {
|
||||
if (!panel.y_formats) {
|
||||
panel.y_formats = [];
|
||||
}
|
||||
panel.y_formats[0] = panel.y_format;
|
||||
delete panel.y_format;
|
||||
}
|
||||
|
||||
if (panel.y2_format) {
|
||||
if (!panel.y_formats) {
|
||||
panel.y_formats = [];
|
||||
}
|
||||
panel.y_formats[1] = panel.y2_format;
|
||||
delete panel.y2_format;
|
||||
}
|
||||
@@ -372,6 +378,11 @@ export class DashboardMigrator {
|
||||
for (j = 0; j < this.dashboard.panels.length; j++) {
|
||||
for (k = 0; k < panelUpgrades.length; k++) {
|
||||
panelUpgrades[k].call(this, this.dashboard.panels[j]);
|
||||
if (this.dashboard.panels[j].panels) {
|
||||
for (n = 0; n < this.dashboard.panels[j].panels.length; n++) {
|
||||
panelUpgrades[k].call(this, this.dashboard.panels[j].panels[n]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,6 +440,9 @@ export class DashboardMigrator {
|
||||
|
||||
for (let panel of row.panels) {
|
||||
panel.span = panel.span || DEFAULT_PANEL_SPAN;
|
||||
if (panel.minSpan) {
|
||||
panel.minSpan = Math.min(GRID_COLUMN_COUNT, GRID_COLUMN_COUNT / 12 * panel.minSpan);
|
||||
}
|
||||
const panelWidth = Math.floor(panel.span) * widthFactor;
|
||||
const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { DashboardMigrator } from './dashboard_migration';
|
||||
|
||||
export class DashboardModel {
|
||||
id: any;
|
||||
uid: any;
|
||||
title: any;
|
||||
autoUpdate: any;
|
||||
description: any;
|
||||
@@ -56,6 +57,7 @@ export class DashboardModel {
|
||||
|
||||
this.events = new Emitter();
|
||||
this.id = data.id || null;
|
||||
this.uid = data.uid || null;
|
||||
this.revision = data.revision;
|
||||
this.title = data.title || 'No Title';
|
||||
this.autoUpdate = data.autoUpdate;
|
||||
@@ -145,7 +147,10 @@ export class DashboardModel {
|
||||
};
|
||||
|
||||
// get panel save models
|
||||
copy.panels = _.map(this.panels, panel => panel.getSaveModel());
|
||||
copy.panels = _.chain(this.panels)
|
||||
.filter(panel => panel.type !== 'add-panel')
|
||||
.map(panel => panel.getSaveModel())
|
||||
.value();
|
||||
|
||||
// sort by keys
|
||||
copy = sortByKeys(copy);
|
||||
@@ -230,10 +235,6 @@ export class DashboardModel {
|
||||
}
|
||||
|
||||
cleanUpRepeats() {
|
||||
this.processRepeats(true);
|
||||
}
|
||||
|
||||
processRepeats(cleanUpOnly?: boolean) {
|
||||
if (this.snapshot || this.templating.list.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -248,11 +249,7 @@ export class DashboardModel {
|
||||
|
||||
for (let i = 0; i < this.panels.length; i++) {
|
||||
let panel = this.panels[i];
|
||||
if (panel.repeat) {
|
||||
if (!cleanUpOnly) {
|
||||
this.repeatPanel(panel, i);
|
||||
}
|
||||
} else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
|
||||
if ((!panel.repeat || panel.repeatedByRow) && panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
|
||||
panelsToRemove.push(panel);
|
||||
}
|
||||
}
|
||||
@@ -264,6 +261,60 @@ export class DashboardModel {
|
||||
this.events.emit('repeats-processed');
|
||||
}
|
||||
|
||||
processRepeats() {
|
||||
if (this.snapshot || this.templating.list.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cleanUpRepeats();
|
||||
|
||||
this.iteration = (this.iteration || new Date().getTime()) + 1;
|
||||
|
||||
for (let i = 0; i < this.panels.length; i++) {
|
||||
let panel = this.panels[i];
|
||||
if (panel.repeat) {
|
||||
this.repeatPanel(panel, i);
|
||||
}
|
||||
}
|
||||
|
||||
this.sortPanelsByGridPos();
|
||||
this.events.emit('repeats-processed');
|
||||
}
|
||||
|
||||
cleanUpRowRepeats(rowPanels) {
|
||||
let panelsToRemove = [];
|
||||
for (let i = 0; i < rowPanels.length; i++) {
|
||||
let panel = rowPanels[i];
|
||||
if (!panel.repeat && panel.repeatPanelId) {
|
||||
panelsToRemove.push(panel);
|
||||
}
|
||||
}
|
||||
_.pull(rowPanels, ...panelsToRemove);
|
||||
_.pull(this.panels, ...panelsToRemove);
|
||||
}
|
||||
|
||||
processRowRepeats(row: PanelModel) {
|
||||
if (this.snapshot || this.templating.list.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rowPanels = row.panels;
|
||||
if (!row.collapsed) {
|
||||
let rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id);
|
||||
rowPanels = this.getRowPanels(rowPanelIndex);
|
||||
}
|
||||
|
||||
this.cleanUpRowRepeats(rowPanels);
|
||||
|
||||
for (let i = 0; i < rowPanels.length; i++) {
|
||||
let panel = rowPanels[i];
|
||||
if (panel.repeat) {
|
||||
let panelIndex = _.findIndex(this.panels, p => p.id === panel.id);
|
||||
this.repeatPanel(panel, panelIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPanelRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) {
|
||||
// if first clone return source
|
||||
if (valueIndex === 0) {
|
||||
@@ -282,21 +333,21 @@ export class DashboardModel {
|
||||
return clone;
|
||||
}
|
||||
|
||||
getRowRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) {
|
||||
getRowRepeatClone(sourceRowPanel, valueIndex, sourcePanelIndex) {
|
||||
// if first clone return source
|
||||
if (valueIndex === 0) {
|
||||
if (!sourcePanel.collapsed) {
|
||||
if (!sourceRowPanel.collapsed) {
|
||||
let rowPanels = this.getRowPanels(sourcePanelIndex);
|
||||
sourcePanel.panels = rowPanels;
|
||||
sourceRowPanel.panels = rowPanels;
|
||||
}
|
||||
return sourcePanel;
|
||||
return sourceRowPanel;
|
||||
}
|
||||
|
||||
let clone = new PanelModel(sourcePanel.getSaveModel());
|
||||
let clone = new PanelModel(sourceRowPanel.getSaveModel());
|
||||
// for row clones we need to figure out panels under row to clone and where to insert clone
|
||||
let rowPanels, insertPos;
|
||||
if (sourcePanel.collapsed) {
|
||||
rowPanels = _.cloneDeep(sourcePanel.panels);
|
||||
if (sourceRowPanel.collapsed) {
|
||||
rowPanels = _.cloneDeep(sourceRowPanel.panels);
|
||||
clone.panels = rowPanels;
|
||||
// insert copied row after preceding row
|
||||
insertPos = sourcePanelIndex + valueIndex;
|
||||
@@ -333,16 +384,17 @@ export class DashboardModel {
|
||||
let copy;
|
||||
|
||||
copy = this.getPanelRepeatClone(panel, index, panelIndex);
|
||||
copy.scopedVars = {};
|
||||
copy.scopedVars = copy.scopedVars || {};
|
||||
copy.scopedVars[variable.name] = option;
|
||||
|
||||
if (panel.repeatDirection === REPEAT_DIR_VERTICAL) {
|
||||
if (index > 0) {
|
||||
yPos += copy.gridPos.h;
|
||||
}
|
||||
copy.gridPos.y = yPos;
|
||||
yPos += copy.gridPos.h;
|
||||
} else {
|
||||
// set width based on how many are selected
|
||||
// assumed the repeated panels should take up full row width
|
||||
|
||||
copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth);
|
||||
copy.gridPos.x = xPos;
|
||||
copy.gridPos.y = yPos;
|
||||
@@ -356,6 +408,15 @@ export class DashboardModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update gridPos for panels below
|
||||
let yOffset = yPos - panel.gridPos.y;
|
||||
if (yOffset > 0) {
|
||||
let panelBelowIndex = panelIndex + selectedOptions.length;
|
||||
for (let i = panelBelowIndex; i < this.panels.length; i++) {
|
||||
this.panels[i].gridPos.y += yOffset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repeatRow(panel: PanelModel, panelIndex: number, variable) {
|
||||
@@ -363,7 +424,7 @@ export class DashboardModel {
|
||||
let yPos = panel.gridPos.y;
|
||||
|
||||
function setScopedVars(panel, variableOption) {
|
||||
panel.scopedVars = {};
|
||||
panel.scopedVars = panel.scopedVars || {};
|
||||
panel.scopedVars[variable.name] = variableOption;
|
||||
}
|
||||
|
||||
@@ -381,7 +442,7 @@ export class DashboardModel {
|
||||
_.each(rowPanels, (rowPanel, i) => {
|
||||
setScopedVars(rowPanel, option);
|
||||
if (optionIndex > 0) {
|
||||
this.updateRepeatedPanelIds(rowPanel);
|
||||
this.updateRepeatedPanelIds(rowPanel, true);
|
||||
}
|
||||
});
|
||||
rowCopy.gridPos.y += optionIndex;
|
||||
@@ -394,7 +455,7 @@ export class DashboardModel {
|
||||
setScopedVars(rowPanel, option);
|
||||
if (optionIndex > 0) {
|
||||
let cloneRowPanel = new PanelModel(rowPanel);
|
||||
this.updateRepeatedPanelIds(cloneRowPanel);
|
||||
this.updateRepeatedPanelIds(cloneRowPanel, true);
|
||||
// For exposed row additionally set proper Y grid position and add it to dashboard panels
|
||||
cloneRowPanel.gridPos.y += rowHeight * optionIndex;
|
||||
this.panels.splice(insertPos + i, 0, cloneRowPanel);
|
||||
@@ -413,11 +474,15 @@ export class DashboardModel {
|
||||
}
|
||||
}
|
||||
|
||||
updateRepeatedPanelIds(panel: PanelModel) {
|
||||
updateRepeatedPanelIds(panel: PanelModel, repeatedByRow?: boolean) {
|
||||
panel.repeatPanelId = panel.id;
|
||||
panel.id = this.getNextPanelId();
|
||||
panel.repeatIteration = this.iteration;
|
||||
panel.repeat = null;
|
||||
if (repeatedByRow) {
|
||||
panel.repeatedByRow = true;
|
||||
} else {
|
||||
panel.repeat = null;
|
||||
}
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -435,11 +500,12 @@ export class DashboardModel {
|
||||
if (!rowPanel.panels || rowPanel.panels.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const rowYPos = rowPanel.gridPos.y;
|
||||
const positions = _.map(rowPanel.panels, 'gridPos');
|
||||
const maxPos = _.maxBy(positions, pos => {
|
||||
return pos.y + pos.h;
|
||||
});
|
||||
return maxPos.h + 1;
|
||||
return maxPos.y + maxPos.h - rowYPos;
|
||||
}
|
||||
|
||||
removePanel(panel: PanelModel) {
|
||||
@@ -458,6 +524,34 @@ export class DashboardModel {
|
||||
this.removePanel(row);
|
||||
}
|
||||
|
||||
expandRows() {
|
||||
for (let i = 0; i < this.panels.length; i++) {
|
||||
var panel = this.panels[i];
|
||||
|
||||
if (panel.type !== 'row') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (panel.collapsed) {
|
||||
this.toggleRow(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collapseRows() {
|
||||
for (let i = 0; i < this.panels.length; i++) {
|
||||
var panel = this.panels[i];
|
||||
|
||||
if (panel.type !== 'row') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!panel.collapsed) {
|
||||
this.toggleRow(panel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setPanelFocus(id) {
|
||||
this.meta.focusPanelId = id;
|
||||
}
|
||||
@@ -540,6 +634,7 @@ export class DashboardModel {
|
||||
|
||||
if (row.collapsed) {
|
||||
row.collapsed = false;
|
||||
let hasRepeat = _.some(row.panels, p => p.repeat);
|
||||
|
||||
if (row.panels.length > 0) {
|
||||
// Use first panel to figure out if it was moved or pushed
|
||||
@@ -570,6 +665,10 @@ export class DashboardModel {
|
||||
}
|
||||
|
||||
row.panels = [];
|
||||
|
||||
if (hasRepeat) {
|
||||
this.processRowRepeats(row);
|
||||
}
|
||||
}
|
||||
|
||||
// sort panels
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardModel } from './dashboard_model';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
export class DashboardSrv {
|
||||
dash: any;
|
||||
@@ -19,7 +20,10 @@ export class DashboardSrv {
|
||||
return this.dash;
|
||||
}
|
||||
|
||||
handleSaveDashboardError(clone, err) {
|
||||
handleSaveDashboardError(clone, options, err) {
|
||||
options = options || {};
|
||||
options.overwrite = true;
|
||||
|
||||
if (err.data && err.data.status === 'version-mismatch') {
|
||||
err.isHandled = true;
|
||||
|
||||
@@ -30,7 +34,7 @@ export class DashboardSrv {
|
||||
yesText: 'Save & Overwrite',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.save(clone, { overwrite: true });
|
||||
this.save(clone, options);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -40,12 +44,12 @@ export class DashboardSrv {
|
||||
|
||||
this.$rootScope.appEvent('confirm-modal', {
|
||||
title: 'Conflict',
|
||||
text: 'Dashboard with the same name exists.',
|
||||
text: 'A dashboard with the same name in selected folder already exists.',
|
||||
text2: 'Would you still like to save this dashboard?',
|
||||
yesText: 'Save & Overwrite',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.save(clone, { overwrite: true });
|
||||
this.save(clone, options);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -73,9 +77,11 @@ export class DashboardSrv {
|
||||
postSave(clone, data) {
|
||||
this.dash.version = data.version;
|
||||
|
||||
var dashboardUrl = '/dashboard/db/' + data.slug;
|
||||
if (dashboardUrl !== this.$location.path()) {
|
||||
this.$location.url(dashboardUrl);
|
||||
const newUrl = locationUtil.stripBaseFromUrl(data.url);
|
||||
const currentPath = this.$location.path();
|
||||
|
||||
if (newUrl !== currentPath) {
|
||||
this.$location.url(newUrl).replace();
|
||||
}
|
||||
|
||||
this.$rootScope.appEvent('dashboard-saved', this.dash);
|
||||
@@ -86,12 +92,12 @@ export class DashboardSrv {
|
||||
|
||||
save(clone, options) {
|
||||
options = options || {};
|
||||
options.folderId = this.dash.meta.folderId;
|
||||
options.folderId = options.folderId >= 0 ? options.folderId : this.dash.meta.folderId || clone.folderId;
|
||||
|
||||
return this.backendSrv
|
||||
.saveDashboard(clone, options)
|
||||
.then(this.postSave.bind(this, clone))
|
||||
.catch(this.handleSaveDashboardError.bind(this, clone));
|
||||
.catch(this.handleSaveDashboardError.bind(this, clone, options));
|
||||
}
|
||||
|
||||
saveDashboard(options, clone) {
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface AddPanelPanelState {
|
||||
export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelPanelState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleCloseAddPanel = this.handleCloseAddPanel.bind(this);
|
||||
this.renderPanelItem = this.renderPanelItem.bind(this);
|
||||
|
||||
this.state = {
|
||||
panelPlugins: this.getPanelPlugins(),
|
||||
@@ -83,8 +85,14 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
||||
dashboard.removePanel(this.props.panel);
|
||||
};
|
||||
|
||||
handleCloseAddPanel(evt) {
|
||||
evt.preventDefault();
|
||||
const panelContainer = this.props.getPanelContainer();
|
||||
const dashboard = panelContainer.getDashboard();
|
||||
dashboard.removePanel(dashboard.panels[0]);
|
||||
}
|
||||
|
||||
renderPanelItem(panel, index) {
|
||||
console.log('render panel', index);
|
||||
return (
|
||||
<div key={index} className="add-panel__item" onClick={() => this.onAddPanel(panel)} title={panel.name}>
|
||||
<img className="add-panel__item-img" src={panel.info.logos.small} />
|
||||
@@ -101,10 +109,11 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
||||
<i className="gicon gicon-add-panel" />
|
||||
<span className="add-panel__title">New Panel</span>
|
||||
<span className="add-panel__sub-title">Select a visualization</span>
|
||||
<button className="add-panel__close" onClick={this.handleCloseAddPanel}>
|
||||
<i className="fa fa-close" />
|
||||
</button>
|
||||
</div>
|
||||
<ScrollBar className="add-panel__items">
|
||||
{this.state.panelPlugins.map(this.renderPanelItem.bind(this))}
|
||||
</ScrollBar>
|
||||
<ScrollBar className="add-panel__items">{this.state.panelPlugins.map(this.renderPanelItem)}</ScrollBar>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import ReactGridLayout from 'react-grid-layout';
|
||||
import ReactGridLayout from 'react-grid-layout-grafana';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { DashboardPanel } from './DashboardPanel';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
@@ -50,7 +50,8 @@ function GridWrapper({
|
||||
onResize={onResize}
|
||||
onResizeStop={onResizeStop}
|
||||
onDragStop={onDragStop}
|
||||
onLayoutChange={onLayoutChange}>
|
||||
onLayoutChange={onLayoutChange}
|
||||
>
|
||||
{children}
|
||||
</ReactGridLayout>
|
||||
);
|
||||
@@ -178,7 +179,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
panelElements.push(
|
||||
<div key={panel.id.toString()} className={panelClasses}>
|
||||
<DashboardPanel panel={panel} getPanelContainer={this.props.getPanelContainer} />
|
||||
</div>,
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,7 +197,8 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
onWidthChange={this.onWidthChange}
|
||||
onDragStop={this.onDragStop}
|
||||
onResize={this.onResize}
|
||||
onResizeStop={this.onResizeStop}>
|
||||
onResizeStop={this.onResizeStop}
|
||||
>
|
||||
{this.renderPanels()}
|
||||
</SizedReactLayoutGrid>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { PanelModel } from "../panel_model";
|
||||
import { PanelContainer } from "./PanelContainer";
|
||||
import templateSrv from "app/features/templating/template_srv";
|
||||
import appEvents from "app/core/app_events";
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { PanelContainer } from './PanelContainer';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export interface DashboardRowProps {
|
||||
panel: PanelModel;
|
||||
@@ -18,7 +19,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
collapsed: this.props.panel.collapsed
|
||||
collapsed: this.props.panel.collapsed,
|
||||
};
|
||||
|
||||
this.panelContainer = this.props.getPanelContainer();
|
||||
@@ -27,6 +28,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.openSettings = this.openSettings.bind(this);
|
||||
this.delete = this.delete.bind(this);
|
||||
this.update = this.update.bind(this);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
@@ -37,23 +39,28 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
this.dashboard.processRepeats();
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
openSettings() {
|
||||
appEvents.emit("show-modal", {
|
||||
appEvents.emit('show-modal', {
|
||||
templateHtml: `<row-options row="model.row" on-updated="model.onUpdated()" dismiss="dismiss()"></row-options>`,
|
||||
modalClass: "modal--narrow",
|
||||
modalClass: 'modal--narrow',
|
||||
model: {
|
||||
row: this.props.panel,
|
||||
onUpdated: this.forceUpdate.bind(this)
|
||||
}
|
||||
onUpdated: this.update.bind(this),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
delete() {
|
||||
appEvents.emit("confirm-modal", {
|
||||
title: "Delete Row",
|
||||
text: "Are you sure you want to remove this row and all its panels?",
|
||||
altActionText: "Delete row only",
|
||||
icon: "fa-trash",
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete Row',
|
||||
text: 'Are you sure you want to remove this row and all its panels?',
|
||||
altActionText: 'Delete row only',
|
||||
icon: 'fa-trash',
|
||||
onConfirm: () => {
|
||||
const panelContainer = this.props.getPanelContainer();
|
||||
const dashboard = panelContainer.getDashboard();
|
||||
@@ -63,46 +70,41 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
const panelContainer = this.props.getPanelContainer();
|
||||
const dashboard = panelContainer.getDashboard();
|
||||
dashboard.removeRow(this.props.panel, false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = classNames({
|
||||
"dashboard-row": true,
|
||||
"dashboard-row--collapsed": this.state.collapsed
|
||||
'dashboard-row': true,
|
||||
'dashboard-row--collapsed': this.state.collapsed,
|
||||
});
|
||||
const chevronClass = classNames({
|
||||
fa: true,
|
||||
"fa-chevron-down": !this.state.collapsed,
|
||||
"fa-chevron-right": this.state.collapsed
|
||||
'fa-chevron-down': !this.state.collapsed,
|
||||
'fa-chevron-right': this.state.collapsed,
|
||||
});
|
||||
|
||||
let title = templateSrv.replaceWithText(
|
||||
this.props.panel.title,
|
||||
this.props.panel.scopedVars
|
||||
);
|
||||
const hiddenPanels = this.props.panel.panels
|
||||
? this.props.panel.panels.length
|
||||
: 0;
|
||||
let title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
|
||||
const hiddenPanels = this.props.panel.panels ? this.props.panel.panels.length : 0;
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<a className="dashboard-row__title pointer" onClick={this.toggle}>
|
||||
<i className={chevronClass} />
|
||||
{title}
|
||||
<span className="dashboard-row__panel_count">
|
||||
({hiddenPanels} hidden panels)
|
||||
</span>
|
||||
<span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
|
||||
</a>
|
||||
<div className="dashboard-row__actions">
|
||||
<a className="pointer" onClick={this.openSettings}>
|
||||
<i className="fa fa-cog" />
|
||||
</a>
|
||||
<a className="pointer" onClick={this.delete}>
|
||||
<i className="fa fa-trash" />
|
||||
</a>
|
||||
</div>
|
||||
{config.bootData.user.orgRole !== 'Viewer' && (
|
||||
<div className="dashboard-row__actions">
|
||||
<a className="pointer" onClick={this.openSettings}>
|
||||
<i className="fa fa-cog" />
|
||||
</a>
|
||||
<a className="pointer" onClick={this.delete}>
|
||||
<i className="fa fa-trash" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="dashboard-row__drag grid-drag-handle" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -35,8 +35,7 @@ export class DashNavCtrl {
|
||||
let search = this.$location.search();
|
||||
if (search.editview) {
|
||||
delete search.editview;
|
||||
}
|
||||
if (search.fullscreen) {
|
||||
} else if (search.fullscreen) {
|
||||
delete search.fullscreen;
|
||||
delete search.edit;
|
||||
}
|
||||
@@ -73,9 +72,10 @@ export class DashNavCtrl {
|
||||
}
|
||||
|
||||
addPanel() {
|
||||
appEvents.emit('dash-scroll', { animate: true, evt: 0 });
|
||||
|
||||
if (this.dashboard.panels.length > 0 && this.dashboard.panels[0].type === 'add-panel') {
|
||||
this.dashboard.removePanel(this.dashboard.panels[0]);
|
||||
return;
|
||||
return; // Return if the "Add panel" exists already
|
||||
}
|
||||
|
||||
this.dashboard.addPanel({
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import { FolderPageLoader } from './folder_page_loader';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
|
||||
export class FolderDashboardsCtrl {
|
||||
navModel: any;
|
||||
folderId: number;
|
||||
uid: string;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, navModelSrv, private $routeParams) {
|
||||
if (this.$routeParams.folderId && this.$routeParams.slug) {
|
||||
this.folderId = $routeParams.folderId;
|
||||
constructor(private backendSrv, navModelSrv, private $routeParams, $location) {
|
||||
if (this.$routeParams.uid) {
|
||||
this.uid = $routeParams.uid;
|
||||
|
||||
const loader = new FolderPageLoader(this.backendSrv, this.$routeParams);
|
||||
const loader = new FolderPageLoader(this.backendSrv);
|
||||
|
||||
loader.load(this, this.folderId, 'manage-folder-dashboards');
|
||||
loader.load(this, this.uid, 'manage-folder-dashboards').then(folder => {
|
||||
const url = locationUtil.stripBaseFromUrl(folder.url);
|
||||
|
||||
if (url !== $location.path()) {
|
||||
$location.path(url).replace();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
public/app/features/dashboard/folder_page_loader.ts
Normal file → Executable file
51
public/app/features/dashboard/folder_page_loader.ts
Normal file → Executable file
@@ -1,9 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class FolderPageLoader {
|
||||
constructor(private backendSrv, private $routeParams) {}
|
||||
constructor(private backendSrv) {}
|
||||
|
||||
load(ctrl, folderId, activeChildId) {
|
||||
load(ctrl, uid, activeChildId) {
|
||||
ctrl.navModel = {
|
||||
main: {
|
||||
icon: 'fa fa-folder-open',
|
||||
@@ -11,60 +9,53 @@ export class FolderPageLoader {
|
||||
subTitle: 'Manage folder dashboards & permissions',
|
||||
url: '',
|
||||
text: '',
|
||||
breadcrumbs: [{ title: 'Dashboards', url: '/dashboards' }, { title: ' ' }],
|
||||
breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }],
|
||||
children: [
|
||||
{
|
||||
active: activeChildId === 'manage-folder-dashboards',
|
||||
icon: 'fa fa-fw fa-th-large',
|
||||
id: 'manage-folder-dashboards',
|
||||
text: 'Dashboards',
|
||||
url: '/dashboards',
|
||||
url: 'dashboards',
|
||||
},
|
||||
{
|
||||
active: activeChildId === 'manage-folder-permissions',
|
||||
icon: 'fa fa-fw fa-lock',
|
||||
id: 'manage-folder-permissions',
|
||||
text: 'Permissions',
|
||||
url: '/dashboards/permissions',
|
||||
url: 'dashboards/permissions',
|
||||
},
|
||||
{
|
||||
active: activeChildId === 'manage-folder-settings',
|
||||
icon: 'fa fa-fw fa-cog',
|
||||
id: 'manage-folder-settings',
|
||||
text: 'Settings',
|
||||
url: '/dashboards/settings',
|
||||
url: 'dashboards/settings',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
return this.backendSrv.getDashboard('db', this.$routeParams.slug).then(result => {
|
||||
const folderTitle = result.dashboard.title;
|
||||
ctrl.navModel.main.text = '';
|
||||
ctrl.navModel.main.breadcrumbs = [{ title: 'Dashboards', url: '/dashboards' }, { title: folderTitle }];
|
||||
return this.backendSrv.getFolderByUid(uid).then(folder => {
|
||||
ctrl.folderId = folder.id;
|
||||
const folderTitle = folder.title;
|
||||
const folderUrl = folder.url;
|
||||
ctrl.navModel.main.text = folderTitle;
|
||||
|
||||
const folderUrl = this.createFolderUrl(folderId, result.meta.type, result.meta.slug);
|
||||
|
||||
const dashTab = _.find(ctrl.navModel.main.children, {
|
||||
id: 'manage-folder-dashboards',
|
||||
});
|
||||
const dashTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-dashboards');
|
||||
dashTab.url = folderUrl;
|
||||
|
||||
const permTab = _.find(ctrl.navModel.main.children, {
|
||||
id: 'manage-folder-permissions',
|
||||
});
|
||||
permTab.url = folderUrl + '/permissions';
|
||||
if (folder.canAdmin) {
|
||||
const permTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-permissions');
|
||||
permTab.url = folderUrl + '/permissions';
|
||||
|
||||
const settingsTab = _.find(ctrl.navModel.main.children, {
|
||||
id: 'manage-folder-settings',
|
||||
});
|
||||
settingsTab.url = folderUrl + '/settings';
|
||||
const settingsTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-settings');
|
||||
settingsTab.url = folderUrl + '/settings';
|
||||
} else {
|
||||
ctrl.navModel.main.children = [dashTab];
|
||||
}
|
||||
|
||||
return result;
|
||||
return folder;
|
||||
});
|
||||
}
|
||||
|
||||
createFolderUrl(folderId: number, type: string, slug: string) {
|
||||
return `dashboards/folder/${folderId}/${slug}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,23 @@ import { FolderPageLoader } from './folder_page_loader';
|
||||
export class FolderPermissionsCtrl {
|
||||
navModel: any;
|
||||
folderId: number;
|
||||
uid: string;
|
||||
dashboard: any;
|
||||
meta: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, navModelSrv, private $routeParams) {
|
||||
if (this.$routeParams.folderId && this.$routeParams.slug) {
|
||||
this.folderId = $routeParams.folderId;
|
||||
constructor(private backendSrv, navModelSrv, private $routeParams, $location) {
|
||||
if (this.$routeParams.uid) {
|
||||
this.uid = $routeParams.uid;
|
||||
|
||||
new FolderPageLoader(this.backendSrv, this.$routeParams).load(this, this.folderId, 'manage-folder-permissions');
|
||||
new FolderPageLoader(this.backendSrv).load(this, this.uid, 'manage-folder-permissions').then(folder => {
|
||||
if ($location.path() !== folder.meta.url) {
|
||||
$location.path(`${folder.meta.url}/permissions`).replace();
|
||||
}
|
||||
|
||||
this.dashboard = folder.dashboard;
|
||||
this.meta = folder.meta;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,29 +9,21 @@
|
||||
</div>
|
||||
<input type="text"
|
||||
class="gf-form-input max-width-10"
|
||||
ng-show="ctrl.createNewFolder"
|
||||
ng-if="ctrl.createNewFolder"
|
||||
give-focus="ctrl.createNewFolder"
|
||||
ng-model="ctrl.newFolderName"
|
||||
ng-model-options="{ debounce: 400 }"
|
||||
ng-class="{'validation-error': !ctrl.isNewFolderNameValid()}"
|
||||
ng-change="ctrl.newFolderNameChanged()" />
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.createNewFolder">
|
||||
<label class="gf-form-label text-success"
|
||||
ng-show="ctrl.newFolderNameTouched && !ctrl.hasValidationError">
|
||||
<i class="fa fa-check"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.createNewFolder">
|
||||
<button class="gf-form-label"
|
||||
<div class="gf-form" ng-if="ctrl.createNewFolder">
|
||||
<button class="btn btn-inverse"
|
||||
ng-click="ctrl.createFolder($event)"
|
||||
ng-disabled="!ctrl.newFolderNameTouched || ctrl.hasValidationError">
|
||||
<i class="fa fa-fw fa-save"></i> Create
|
||||
</button>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.createNewFolder">
|
||||
<button class="gf-form-label"
|
||||
ng-click="ctrl.cancelCreateFolder($event)">
|
||||
<div class="gf-form" ng-if="ctrl.createNewFolder">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.cancelCreateFolder($event)">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ export class FolderPickerCtrl {
|
||||
enterFolderCreation: any;
|
||||
exitFolderCreation: any;
|
||||
enableCreateNew: boolean;
|
||||
rootName = 'Root';
|
||||
rootName = 'General';
|
||||
folder: any;
|
||||
createNewFolder: boolean;
|
||||
newFolderName: string;
|
||||
@@ -30,18 +30,22 @@ export class FolderPickerCtrl {
|
||||
}
|
||||
|
||||
getOptions(query) {
|
||||
var params = {
|
||||
const params = {
|
||||
query: query,
|
||||
type: 'dash-folder',
|
||||
permission: 'Edit',
|
||||
};
|
||||
|
||||
return this.backendSrv.search(params).then(result => {
|
||||
return this.backendSrv.get('api/search', params).then(result => {
|
||||
if (
|
||||
query === '' ||
|
||||
query.toLowerCase() === 'r' ||
|
||||
query.toLowerCase() === 'ro' ||
|
||||
query.toLowerCase() === 'roo' ||
|
||||
query.toLowerCase() === 'root'
|
||||
query.toLowerCase() === 'g' ||
|
||||
query.toLowerCase() === 'ge' ||
|
||||
query.toLowerCase() === 'gen' ||
|
||||
query.toLowerCase() === 'gene' ||
|
||||
query.toLowerCase() === 'gener' ||
|
||||
query.toLowerCase() === 'genera' ||
|
||||
query.toLowerCase() === 'general'
|
||||
) {
|
||||
result.unshift({ title: this.rootName, id: 0 });
|
||||
}
|
||||
@@ -69,7 +73,7 @@ export class FolderPickerCtrl {
|
||||
this.newFolderNameTouched = true;
|
||||
|
||||
this.validationSrv
|
||||
.validateNewDashboardOrFolderName(this.newFolderName)
|
||||
.validateNewFolderName(this.newFolderName)
|
||||
.then(() => {
|
||||
this.hasValidationError = false;
|
||||
})
|
||||
@@ -85,13 +89,13 @@ export class FolderPickerCtrl {
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
return this.backendSrv.createDashboardFolder(this.newFolderName).then(result => {
|
||||
return this.backendSrv.createFolder({ title: this.newFolderName }).then(result => {
|
||||
appEvents.emit('alert-success', ['Folder Created', 'OK']);
|
||||
|
||||
this.closeCreateFolder();
|
||||
this.folder = {
|
||||
text: result.dashboard.title,
|
||||
value: result.dashboard.id,
|
||||
text: result.title,
|
||||
value: result.id,
|
||||
};
|
||||
this.onFolderChange(this.folder);
|
||||
});
|
||||
@@ -120,6 +124,9 @@ export class FolderPickerCtrl {
|
||||
if (this.initialFolderId && this.initialFolderId > 0) {
|
||||
this.getOptions('').then(result => {
|
||||
this.folder = _.find(result, { value: this.initialFolderId });
|
||||
if (!this.folder) {
|
||||
this.folder = { text: this.initialTitle, value: this.initialFolderId };
|
||||
}
|
||||
this.onFolderLoad();
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -5,23 +5,26 @@ export class FolderSettingsCtrl {
|
||||
folderPageLoader: FolderPageLoader;
|
||||
navModel: any;
|
||||
folderId: number;
|
||||
uid: string;
|
||||
canSave = false;
|
||||
dashboard: any;
|
||||
meta: any;
|
||||
folder: any;
|
||||
title: string;
|
||||
hasChanged: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, navModelSrv, private $routeParams, private $location) {
|
||||
if (this.$routeParams.folderId && this.$routeParams.slug) {
|
||||
this.folderId = $routeParams.folderId;
|
||||
if (this.$routeParams.uid) {
|
||||
this.uid = $routeParams.uid;
|
||||
|
||||
this.folderPageLoader = new FolderPageLoader(this.backendSrv, this.$routeParams);
|
||||
this.folderPageLoader.load(this, this.folderId, 'manage-folder-settings').then(result => {
|
||||
this.dashboard = result.dashboard;
|
||||
this.meta = result.meta;
|
||||
this.canSave = result.meta.canSave;
|
||||
this.title = this.dashboard.title;
|
||||
this.folderPageLoader = new FolderPageLoader(this.backendSrv);
|
||||
this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => {
|
||||
if ($location.path() !== folder.meta.url) {
|
||||
$location.path(`${folder.meta.url}/settings`).replace();
|
||||
}
|
||||
|
||||
this.folder = folder;
|
||||
this.canSave = this.folder.canSave;
|
||||
this.title = this.folder.title;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -33,14 +36,13 @@ export class FolderSettingsCtrl {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dashboard.title = this.title.trim();
|
||||
this.folder.title = this.title.trim();
|
||||
|
||||
return this.backendSrv
|
||||
.saveDashboard(this.dashboard, { overwrite: false })
|
||||
.updateFolder(this.folder)
|
||||
.then(result => {
|
||||
var folderUrl = this.folderPageLoader.createFolderUrl(this.folderId, this.meta.type, result.slug);
|
||||
if (folderUrl !== this.$location.path()) {
|
||||
this.$location.url(folderUrl + '/settings');
|
||||
if (result.url !== this.$location.path()) {
|
||||
this.$location.url(result.url + '/settings');
|
||||
}
|
||||
|
||||
appEvents.emit('dashboard-saved');
|
||||
@@ -50,7 +52,7 @@ export class FolderSettingsCtrl {
|
||||
}
|
||||
|
||||
titleChanged() {
|
||||
this.hasChanged = this.dashboard.title.toLowerCase() !== this.title.trim().toLowerCase();
|
||||
this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase();
|
||||
}
|
||||
|
||||
delete(evt) {
|
||||
@@ -65,8 +67,8 @@ export class FolderSettingsCtrl {
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: () => {
|
||||
return this.backendSrv.deleteDashboard(this.meta.slug).then(() => {
|
||||
appEvents.emit('alert-success', ['Folder Deleted', `${this.dashboard.title} has been deleted`]);
|
||||
return this.backendSrv.deleteFolder(this.uid).then(() => {
|
||||
appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]);
|
||||
this.$location.url('dashboards');
|
||||
});
|
||||
},
|
||||
@@ -84,15 +86,9 @@ export class FolderSettingsCtrl {
|
||||
yesText: 'Save & Overwrite',
|
||||
icon: 'fa-warning',
|
||||
onConfirm: () => {
|
||||
this.backendSrv.saveDashboard(this.dashboard, { overwrite: true });
|
||||
this.backendSrv.updateFolder(this.folder, { overwrite: true });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (err.data && err.data.status === 'name-exists') {
|
||||
err.isHandled = true;
|
||||
|
||||
appEvents.emit('alert-error', ['A folder or dashboard with this name exists already.']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import _ from 'lodash';
|
||||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv } from './history_srv';
|
||||
|
||||
@@ -185,7 +186,7 @@ export class HistoryListCtrl {
|
||||
return this.historySrv
|
||||
.restoreDashboard(this.dashboard, version)
|
||||
.then(response => {
|
||||
this.$location.path('dashboard/db/' + response.slug);
|
||||
this.$location.url(locationUtil.stripBaseFromUrl(response.url)).replace();
|
||||
this.$route.reload();
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard restored', 'Restored from version ' + version]);
|
||||
})
|
||||
|
||||
@@ -26,6 +26,7 @@ export class PanelModel {
|
||||
repeatIteration?: number;
|
||||
repeatPanelId?: number;
|
||||
repeatDirection?: string;
|
||||
repeatedByRow?: boolean;
|
||||
minSpan?: number;
|
||||
collapsed?: boolean;
|
||||
panels?: any;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<page-header ng-if="ctrl.navModel" model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<manage-dashboards ng-if="ctrl.folderId" folder-id="ctrl.folderId" />
|
||||
</div>
|
||||
<manage-dashboards ng-if="ctrl.folderId && ctrl.uid" folder-id="ctrl.folderId" folder-uid="ctrl.uid" />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<h2 class="page-sub-heading">
|
||||
Coming soon! Permissions will be added in Grafana 5.0 beta.
|
||||
</h2>
|
||||
<dashboard-permissions ng-if="ctrl.dashboard && ctrl.meta"
|
||||
dashboardId="ctrl.dashboard.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ const template = `
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content" novalidate>
|
||||
<form name="ctrl.saveForm" class="modal-content" novalidate>
|
||||
<div class="p-t-2">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">New name</label>
|
||||
@@ -31,7 +31,7 @@ const template = `
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row text-center">
|
||||
<button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid || !ctrl.isValidFolderSelection">Save</button>
|
||||
<button type="submit" class="btn btn-success" ng-click="ctrl.save()" ng-disabled="!ctrl.isValidFolderSelection">Save</button>
|
||||
<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -41,14 +41,15 @@ const template = `
|
||||
export class SaveDashboardAsModalCtrl {
|
||||
clone: any;
|
||||
folderId: any;
|
||||
isValidFolderSelection = true;
|
||||
dismiss: () => void;
|
||||
isValidFolderSelection = true;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private dashboardSrv) {
|
||||
var dashboard = this.dashboardSrv.getCurrent();
|
||||
this.clone = dashboard.getSaveModelClone();
|
||||
this.clone.id = null;
|
||||
this.clone.uid = '';
|
||||
this.clone.title += ' Copy';
|
||||
this.clone.editable = true;
|
||||
this.clone.hideControls = false;
|
||||
@@ -69,7 +70,17 @@ export class SaveDashboardAsModalCtrl {
|
||||
}
|
||||
|
||||
save() {
|
||||
return this.dashboardSrv.save(this.clone).then(this.dismiss);
|
||||
return this.dashboardSrv.save(this.clone, { folderId: this.folderId }).then(this.dismiss);
|
||||
}
|
||||
|
||||
keyDown(evt) {
|
||||
if (evt.keyCode === 13) {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
onFolderChange(folder) {
|
||||
this.folderId = folder.id;
|
||||
}
|
||||
|
||||
onEnterFolderCreation() {
|
||||
@@ -79,16 +90,6 @@ export class SaveDashboardAsModalCtrl {
|
||||
onExitFolderCreation() {
|
||||
this.isValidFolderSelection = true;
|
||||
}
|
||||
|
||||
keyDown(evt) {
|
||||
if (this.isValidFolderSelection && evt.keyCode === 13) {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
onFolderChange(folder) {
|
||||
this.clone.folderId = folder.id;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveDashboardAsDirective() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
const template = `
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
</a>
|
||||
|
||||
<div class="dashboard-settings__aside-actions">
|
||||
<button class="btn btn-success" ng-click="ctrl.saveDashboard()" ng-show="ctrl.canSave">
|
||||
<i class="fa fa-save"></i> Save
|
||||
</button>
|
||||
<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
|
||||
@@ -93,6 +95,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'permissions'" >
|
||||
<dashboard-permissions ng-if="ctrl.dashboard && !ctrl.hasUnsavedFolderChange"
|
||||
dashboardId="ctrl.dashboard.id"
|
||||
backendSrv="ctrl.backendSrv"
|
||||
folder="ctrl.getFolder()"
|
||||
/>
|
||||
<div ng-if="ctrl.hasUnsavedFolderChange">
|
||||
<h5>You have changed folder, please save to view permissions.</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === '404'">
|
||||
<h3 class="dashboard-settings__header">Settings view not found</h3>
|
||||
|
||||
|
||||
40
public/app/features/dashboard/settings/settings.ts
Normal file → Executable file
40
public/app/features/dashboard/settings/settings.ts
Normal file → Executable file
@@ -2,6 +2,7 @@ import { coreModule, appEvents, contextSrv } from 'app/core/core';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
|
||||
export class SettingsCtrl {
|
||||
dashboard: DashboardModel;
|
||||
@@ -10,8 +11,10 @@ export class SettingsCtrl {
|
||||
json: string;
|
||||
alertCount: number;
|
||||
canSaveAs: boolean;
|
||||
canSave: boolean;
|
||||
canDelete: boolean;
|
||||
sections: any[];
|
||||
hasUnsavedFolderChange: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $scope, private $location, private $rootScope, private backendSrv, private dashboardSrv) {
|
||||
@@ -22,15 +25,21 @@ export class SettingsCtrl {
|
||||
this.$scope.$on('$destroy', () => {
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
setTimeout(() => {
|
||||
this.$rootScope.appEvent('dash-scroll', { restore: true });
|
||||
});
|
||||
});
|
||||
|
||||
this.canSaveAs = contextSrv.isEditor;
|
||||
this.canSave = this.dashboard.meta.canSave;
|
||||
this.canDelete = this.dashboard.meta.canSave;
|
||||
|
||||
this.buildSectionList();
|
||||
this.onRouteUpdated();
|
||||
|
||||
$rootScope.onAppEvent('$routeUpdate', this.onRouteUpdated.bind(this), $scope);
|
||||
this.$rootScope.onAppEvent('$routeUpdate', this.onRouteUpdated.bind(this), $scope);
|
||||
this.$rootScope.appEvent('dash-scroll', { animate: false, pos: 0 });
|
||||
this.$rootScope.onAppEvent('dashboard-saved', this.onPostSave.bind(this), $scope);
|
||||
}
|
||||
|
||||
buildSectionList() {
|
||||
@@ -67,6 +76,14 @@ export class SettingsCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.dashboard.id && this.dashboard.meta.canAdmin) {
|
||||
this.sections.push({
|
||||
title: 'Permissions',
|
||||
id: 'permissions',
|
||||
icon: 'fa fa-fw fa-lock',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.dashboard.meta.canMakeEditable) {
|
||||
this.sections.push({
|
||||
title: 'General',
|
||||
@@ -86,7 +103,7 @@ export class SettingsCtrl {
|
||||
|
||||
for (let section of this.sections) {
|
||||
const sectionParams = _.defaults({ editview: section.id }, params);
|
||||
section.url = url + '?' + $.param(sectionParams);
|
||||
section.url = config.appSubUrl + url + '?' + $.param(sectionParams);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +133,14 @@ export class SettingsCtrl {
|
||||
this.dashboardSrv.showSaveAsModal();
|
||||
}
|
||||
|
||||
saveDashboard() {
|
||||
this.dashboardSrv.saveDashboard();
|
||||
}
|
||||
|
||||
onPostSave() {
|
||||
this.hasUnsavedFolderChange = false;
|
||||
}
|
||||
|
||||
hideSettings() {
|
||||
var urlParams = this.$location.search();
|
||||
delete urlParams.editview;
|
||||
@@ -167,7 +192,7 @@ export class SettingsCtrl {
|
||||
}
|
||||
|
||||
deleteDashboardConfirmed() {
|
||||
this.backendSrv.deleteDashboard(this.dashboard.meta.slug).then(() => {
|
||||
this.backendSrv.deleteDashboard(this.dashboard.uid).then(() => {
|
||||
appEvents.emit('alert-success', ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
|
||||
this.$location.url('/');
|
||||
});
|
||||
@@ -176,6 +201,15 @@ export class SettingsCtrl {
|
||||
onFolderChange(folder) {
|
||||
this.dashboard.meta.folderId = folder.id;
|
||||
this.dashboard.meta.folderTitle = folder.title;
|
||||
this.hasUnsavedFolderChange = true;
|
||||
}
|
||||
|
||||
getFolder() {
|
||||
return {
|
||||
id: this.dashboard.meta.folderId,
|
||||
title: this.dashboard.meta.folderTitle,
|
||||
url: this.dashboard.meta.folderUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ export class ShareModalCtrl {
|
||||
$scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
|
||||
|
||||
var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
|
||||
soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/');
|
||||
delete params.fullscreen;
|
||||
delete params.edit;
|
||||
soloUrl = linkSrv.addParamsToUrl(soloUrl, params);
|
||||
@@ -84,6 +85,7 @@ export class ShareModalCtrl {
|
||||
config.appSubUrl + '/dashboard-solo/',
|
||||
config.appSubUrl + '/render/dashboard-solo/'
|
||||
);
|
||||
$scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/');
|
||||
$scope.imageUrl += '&width=1000';
|
||||
$scope.imageUrl += '&height=500';
|
||||
$scope.imageUrl += '&tz=UTC' + encodeURIComponent(moment().format('Z'));
|
||||
|
||||
@@ -2,6 +2,7 @@ import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class ShareSnapshotCtrl {
|
||||
/** @ngInject **/
|
||||
constructor($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) {
|
||||
$scope.snapshot = {
|
||||
name: $scope.dashboard.title,
|
||||
|
||||
@@ -2,19 +2,26 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { DashboardRow } from '../dashgrid/DashboardRow';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import config from '../../../core/config';
|
||||
|
||||
describe('DashboardRow', () => {
|
||||
let wrapper, panel, getPanelContainer, dashboardMock;
|
||||
|
||||
beforeEach(() => {
|
||||
dashboardMock = {toggleRow: jest.fn()};
|
||||
dashboardMock = { toggleRow: jest.fn() };
|
||||
|
||||
config.bootData = {
|
||||
user: {
|
||||
orgRole: 'Admin',
|
||||
},
|
||||
};
|
||||
|
||||
getPanelContainer = jest.fn().mockReturnValue({
|
||||
getDashboard: jest.fn().mockReturnValue(dashboardMock),
|
||||
getPanelLoader: jest.fn()
|
||||
getPanelLoader: jest.fn(),
|
||||
});
|
||||
|
||||
panel = new PanelModel({collapsed: false});
|
||||
panel = new PanelModel({ collapsed: false });
|
||||
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
||||
});
|
||||
|
||||
@@ -30,4 +37,14 @@ describe('DashboardRow', () => {
|
||||
expect(dashboardMock.toggleRow.mock.calls).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should have two actions as admin', () => {
|
||||
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should have zero actions as viewer', () => {
|
||||
config.bootData.user.orgRole = 'Viewer';
|
||||
panel = new PanelModel({ collapsed: false });
|
||||
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
||||
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,10 +19,10 @@ describe('DashboardImportCtrl', function() {
|
||||
};
|
||||
|
||||
validationSrv = {
|
||||
validateNewDashboardOrFolderName: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
};
|
||||
|
||||
ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {}, {});
|
||||
ctx.ctrl = new DashboardImportCtrl(backendSrv, validationSrv, navModelSrv, {}, {});
|
||||
});
|
||||
|
||||
describe('when uploading json', function() {
|
||||
|
||||
@@ -363,6 +363,22 @@ describe('DashboardModel', function() {
|
||||
expect(dashboard.panels[0].repeat).toBe('server');
|
||||
expect(dashboard.panels.length).toBe(2);
|
||||
});
|
||||
|
||||
it('minSpan should be twice', function() {
|
||||
model.rows = [createRow({ height: 8 }, [[6]])];
|
||||
model.rows[0].panels[0] = { minSpan: 12 };
|
||||
|
||||
let dashboard = new DashboardModel(model);
|
||||
expect(dashboard.panels[0].minSpan).toBe(24);
|
||||
});
|
||||
|
||||
it('should assign id', function() {
|
||||
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
|
||||
model.rows[0].panels[0] = {};
|
||||
|
||||
let dashboard = new DashboardModel(model);
|
||||
expect(dashboard.panels[0].id).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -49,6 +49,23 @@ describe('DashboardModel', function() {
|
||||
expect(keys[0]).toBe('annotations');
|
||||
expect(keys[1]).toBe('autoUpdate');
|
||||
});
|
||||
|
||||
it('should remove add panel panels', () => {
|
||||
var model = new DashboardModel({});
|
||||
model.addPanel({
|
||||
type: 'add-panel',
|
||||
});
|
||||
model.addPanel({
|
||||
type: 'graph',
|
||||
});
|
||||
model.addPanel({
|
||||
type: 'add-panel',
|
||||
});
|
||||
var saveModel = model.getSaveModelClone();
|
||||
var panels = saveModel.panels;
|
||||
|
||||
expect(panels.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('row and panel manipulation', function() {
|
||||
|
||||
@@ -4,6 +4,57 @@ import { expect } from 'test/lib/common';
|
||||
|
||||
jest.mock('app/core/services/context_srv', () => ({}));
|
||||
|
||||
describe('given dashboard with panel repeat', function() {
|
||||
var dashboard;
|
||||
|
||||
beforeEach(function() {
|
||||
let dashboardJSON = {
|
||||
panels: [
|
||||
{ id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } },
|
||||
{ id: 2, repeat: 'apps', repeatDirection: 'h', gridPos: { x: 0, y: 1, h: 2, w: 8 } },
|
||||
],
|
||||
templating: {
|
||||
list: [
|
||||
{
|
||||
name: 'apps',
|
||||
current: {
|
||||
text: 'se1, se2, se3',
|
||||
value: ['se1', 'se2', 'se3'],
|
||||
},
|
||||
options: [
|
||||
{ text: 'se1', value: 'se1', selected: true },
|
||||
{ text: 'se2', value: 'se2', selected: true },
|
||||
{ text: 'se3', value: 'se3', selected: true },
|
||||
{ text: 'se4', value: 'se4', selected: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
dashboard = new DashboardModel(dashboardJSON);
|
||||
dashboard.processRepeats();
|
||||
});
|
||||
|
||||
it('should repeat panels when row is expanding', function() {
|
||||
expect(dashboard.panels.length).toBe(4);
|
||||
|
||||
// toggle row
|
||||
dashboard.toggleRow(dashboard.panels[0]);
|
||||
expect(dashboard.panels.length).toBe(1);
|
||||
|
||||
// change variable
|
||||
dashboard.templating.list[0].options[2].selected = false;
|
||||
dashboard.templating.list[0].current = {
|
||||
text: 'se1, se2',
|
||||
value: ['se1', 'se2'],
|
||||
};
|
||||
|
||||
// toggle row back
|
||||
dashboard.toggleRow(dashboard.panels[0]);
|
||||
expect(dashboard.panels.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given dashboard with panel repeat in horizontal direction', function() {
|
||||
var dashboard;
|
||||
|
||||
@@ -142,12 +193,9 @@ describe('given dashboard with panel repeat in vertical direction', function() {
|
||||
beforeEach(function() {
|
||||
dashboard = new DashboardModel({
|
||||
panels: [
|
||||
{
|
||||
id: 2,
|
||||
repeat: 'apps',
|
||||
repeatDirection: 'v',
|
||||
gridPos: { x: 5, y: 0, h: 2, w: 8 },
|
||||
},
|
||||
{ id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } },
|
||||
{ id: 2, repeat: 'apps', repeatDirection: 'v', gridPos: { x: 5, y: 1, h: 2, w: 8 } },
|
||||
{ id: 3, type: 'row', gridPos: { x: 0, y: 3, h: 1, w: 24 } },
|
||||
],
|
||||
templating: {
|
||||
list: [
|
||||
@@ -171,24 +219,95 @@ describe('given dashboard with panel repeat in vertical direction', function() {
|
||||
});
|
||||
|
||||
it('should place on items on top of each other and keep witdh', function() {
|
||||
expect(dashboard.panels[0].gridPos).toMatchObject({
|
||||
x: 5,
|
||||
y: 0,
|
||||
h: 2,
|
||||
w: 8,
|
||||
});
|
||||
expect(dashboard.panels[1].gridPos).toMatchObject({
|
||||
x: 5,
|
||||
y: 2,
|
||||
h: 2,
|
||||
w: 8,
|
||||
});
|
||||
expect(dashboard.panels[2].gridPos).toMatchObject({
|
||||
x: 5,
|
||||
y: 4,
|
||||
h: 2,
|
||||
w: 8,
|
||||
});
|
||||
expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, h: 1, w: 24 }); // first row
|
||||
|
||||
expect(dashboard.panels[1].gridPos).toMatchObject({ x: 5, y: 1, h: 2, w: 8 });
|
||||
expect(dashboard.panels[2].gridPos).toMatchObject({ x: 5, y: 3, h: 2, w: 8 });
|
||||
expect(dashboard.panels[3].gridPos).toMatchObject({ x: 5, y: 5, h: 2, w: 8 });
|
||||
|
||||
expect(dashboard.panels[4].gridPos).toMatchObject({ x: 0, y: 7, h: 1, w: 24 }); // last row
|
||||
});
|
||||
});
|
||||
|
||||
describe('given dashboard with row repeat and panel repeat in horizontal direction', () => {
|
||||
let dashboard, dashboardJSON;
|
||||
|
||||
beforeEach(() => {
|
||||
dashboardJSON = {
|
||||
panels: [
|
||||
{ id: 1, type: 'row', repeat: 'region', gridPos: { x: 0, y: 0, h: 1, w: 24 } },
|
||||
{ id: 2, type: 'graph', repeat: 'app', gridPos: { x: 0, y: 1, h: 2, w: 6 } },
|
||||
],
|
||||
templating: {
|
||||
list: [
|
||||
{
|
||||
name: 'region',
|
||||
current: {
|
||||
text: 'reg1, reg2',
|
||||
value: ['reg1', 'reg2'],
|
||||
},
|
||||
options: [{ text: 'reg1', value: 'reg1', selected: true }, { text: 'reg2', value: 'reg2', selected: true }],
|
||||
},
|
||||
{
|
||||
name: 'app',
|
||||
current: {
|
||||
text: 'se1, se2, se3, se4, se5, se6',
|
||||
value: ['se1', 'se2', 'se3', 'se4', 'se5', 'se6'],
|
||||
},
|
||||
options: [
|
||||
{ text: 'se1', value: 'se1', selected: true },
|
||||
{ text: 'se2', value: 'se2', selected: true },
|
||||
{ text: 'se3', value: 'se3', selected: true },
|
||||
{ text: 'se4', value: 'se4', selected: true },
|
||||
{ text: 'se5', value: 'se5', selected: true },
|
||||
{ text: 'se6', value: 'se6', selected: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
dashboard = new DashboardModel(dashboardJSON);
|
||||
dashboard.processRepeats(false);
|
||||
});
|
||||
|
||||
it('should panels in self row', () => {
|
||||
const panel_types = _.map(dashboard.panels, 'type');
|
||||
expect(panel_types).toEqual([
|
||||
'row',
|
||||
'graph',
|
||||
'graph',
|
||||
'graph',
|
||||
'graph',
|
||||
'graph',
|
||||
'graph',
|
||||
'row',
|
||||
'graph',
|
||||
'graph',
|
||||
'graph',
|
||||
'graph',
|
||||
'graph',
|
||||
'graph',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be placed in their places', function() {
|
||||
expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, h: 1, w: 24 }); // 1st row
|
||||
|
||||
expect(dashboard.panels[1].gridPos).toMatchObject({ x: 0, y: 1, h: 2, w: 6 });
|
||||
expect(dashboard.panels[2].gridPos).toMatchObject({ x: 6, y: 1, h: 2, w: 6 });
|
||||
expect(dashboard.panels[3].gridPos).toMatchObject({ x: 12, y: 1, h: 2, w: 6 });
|
||||
expect(dashboard.panels[4].gridPos).toMatchObject({ x: 18, y: 1, h: 2, w: 6 });
|
||||
expect(dashboard.panels[5].gridPos).toMatchObject({ x: 0, y: 3, h: 2, w: 6 }); // next row
|
||||
expect(dashboard.panels[6].gridPos).toMatchObject({ x: 6, y: 3, h: 2, w: 6 });
|
||||
|
||||
expect(dashboard.panels[7].gridPos).toMatchObject({ x: 0, y: 5, h: 1, w: 24 });
|
||||
|
||||
expect(dashboard.panels[8].gridPos).toMatchObject({ x: 0, y: 6, h: 2, w: 6 }); // 2nd row
|
||||
expect(dashboard.panels[9].gridPos).toMatchObject({ x: 6, y: 6, h: 2, w: 6 });
|
||||
expect(dashboard.panels[10].gridPos).toMatchObject({ x: 12, y: 6, h: 2, w: 6 });
|
||||
expect(dashboard.panels[11].gridPos).toMatchObject({ x: 18, y: 6, h: 2, w: 6 }); // next row
|
||||
expect(dashboard.panels[12].gridPos).toMatchObject({ x: 0, y: 8, h: 2, w: 6 });
|
||||
expect(dashboard.panels[13].gridPos).toMatchObject({ x: 6, y: 8, h: 2, w: 6 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -381,4 +500,168 @@ describe('given dashboard with row repeat', function() {
|
||||
);
|
||||
expect(panel_ids.length).toEqual(_.uniq(panel_ids).length);
|
||||
});
|
||||
|
||||
it('should place new panels in proper order', function() {
|
||||
dashboardJSON.panels = [
|
||||
{ id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 }, repeat: 'apps' },
|
||||
{ id: 2, type: 'graph', gridPos: { x: 0, y: 1, h: 3, w: 12 } },
|
||||
{ id: 3, type: 'graph', gridPos: { x: 6, y: 1, h: 4, w: 12 } },
|
||||
{ id: 4, type: 'graph', gridPos: { x: 0, y: 5, h: 2, w: 12 } },
|
||||
];
|
||||
dashboard = new DashboardModel(dashboardJSON);
|
||||
dashboard.processRepeats();
|
||||
|
||||
const panel_types = _.map(dashboard.panels, 'type');
|
||||
expect(panel_types).toEqual(['row', 'graph', 'graph', 'graph', 'row', 'graph', 'graph', 'graph']);
|
||||
const panel_y_positions = _.map(dashboard.panels, p => p.gridPos.y);
|
||||
expect(panel_y_positions).toEqual([0, 1, 1, 5, 7, 8, 8, 12]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given dashboard with row and panel repeat', () => {
|
||||
let dashboard, dashboardJSON;
|
||||
|
||||
beforeEach(() => {
|
||||
dashboardJSON = {
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'row',
|
||||
repeat: 'region',
|
||||
gridPos: { x: 0, y: 0, h: 1, w: 24 },
|
||||
},
|
||||
{ id: 2, type: 'graph', repeat: 'app', gridPos: { x: 0, y: 1, h: 1, w: 6 } },
|
||||
],
|
||||
templating: {
|
||||
list: [
|
||||
{
|
||||
name: 'region',
|
||||
current: {
|
||||
text: 'reg1, reg2',
|
||||
value: ['reg1', 'reg2'],
|
||||
},
|
||||
options: [
|
||||
{ text: 'reg1', value: 'reg1', selected: true },
|
||||
{ text: 'reg2', value: 'reg2', selected: true },
|
||||
{ text: 'reg3', value: 'reg3', selected: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'app',
|
||||
current: {
|
||||
text: 'se1, se2',
|
||||
value: ['se1', 'se2'],
|
||||
},
|
||||
options: [
|
||||
{ text: 'se1', value: 'se1', selected: true },
|
||||
{ text: 'se2', value: 'se2', selected: true },
|
||||
{ text: 'se3', value: 'se3', selected: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
dashboard = new DashboardModel(dashboardJSON);
|
||||
dashboard.processRepeats();
|
||||
});
|
||||
|
||||
it('should repeat row and panels for each row', () => {
|
||||
const panel_types = _.map(dashboard.panels, 'type');
|
||||
expect(panel_types).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph']);
|
||||
});
|
||||
|
||||
it('should clean up old repeated panels', () => {
|
||||
dashboardJSON.panels = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'row',
|
||||
repeat: 'region',
|
||||
gridPos: { x: 0, y: 0, h: 1, w: 24 },
|
||||
},
|
||||
{ id: 2, type: 'graph', repeat: 'app', gridPos: { x: 0, y: 1, h: 1, w: 6 } },
|
||||
{ id: 3, type: 'graph', repeatPanelId: 2, repeatIteration: 101, gridPos: { x: 7, y: 1, h: 1, w: 6 } },
|
||||
{
|
||||
id: 11,
|
||||
type: 'row',
|
||||
repeatPanelId: 1,
|
||||
repeatIteration: 101,
|
||||
gridPos: { x: 0, y: 2, h: 1, w: 24 },
|
||||
},
|
||||
{ id: 12, type: 'graph', repeatPanelId: 2, repeatIteration: 101, gridPos: { x: 0, y: 3, h: 1, w: 6 } },
|
||||
];
|
||||
dashboard = new DashboardModel(dashboardJSON);
|
||||
dashboard.processRepeats();
|
||||
|
||||
const panel_types = _.map(dashboard.panels, 'type');
|
||||
expect(panel_types).toEqual(['row', 'graph', 'graph', 'row', 'graph', 'graph']);
|
||||
});
|
||||
|
||||
it('should set scopedVars for each row', () => {
|
||||
dashboard = new DashboardModel(dashboardJSON);
|
||||
dashboard.processRepeats();
|
||||
|
||||
expect(dashboard.panels[0].scopedVars).toMatchObject({
|
||||
region: { text: 'reg1', value: 'reg1' },
|
||||
});
|
||||
expect(dashboard.panels[3].scopedVars).toMatchObject({
|
||||
region: { text: 'reg2', value: 'reg2' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should set panel-repeat variable for each panel', () => {
|
||||
dashboard = new DashboardModel(dashboardJSON);
|
||||
dashboard.processRepeats();
|
||||
|
||||
expect(dashboard.panels[1].scopedVars).toMatchObject({
|
||||
app: { text: 'se1', value: 'se1' },
|
||||
});
|
||||
expect(dashboard.panels[2].scopedVars).toMatchObject({
|
||||
app: { text: 'se2', value: 'se2' },
|
||||
});
|
||||
|
||||
expect(dashboard.panels[4].scopedVars).toMatchObject({
|
||||
app: { text: 'se1', value: 'se1' },
|
||||
});
|
||||
expect(dashboard.panels[5].scopedVars).toMatchObject({
|
||||
app: { text: 'se2', value: 'se2' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should set row-repeat variable for each panel', () => {
|
||||
dashboard = new DashboardModel(dashboardJSON);
|
||||
dashboard.processRepeats();
|
||||
|
||||
expect(dashboard.panels[1].scopedVars).toMatchObject({
|
||||
region: { text: 'reg1', value: 'reg1' },
|
||||
});
|
||||
expect(dashboard.panels[2].scopedVars).toMatchObject({
|
||||
region: { text: 'reg1', value: 'reg1' },
|
||||
});
|
||||
|
||||
expect(dashboard.panels[4].scopedVars).toMatchObject({
|
||||
region: { text: 'reg2', value: 'reg2' },
|
||||
});
|
||||
expect(dashboard.panels[5].scopedVars).toMatchObject({
|
||||
region: { text: 'reg2', value: 'reg2' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat panels when row is expanding', function() {
|
||||
dashboard = new DashboardModel(dashboardJSON);
|
||||
dashboard.processRepeats();
|
||||
|
||||
expect(dashboard.panels.length).toBe(6);
|
||||
|
||||
// toggle row
|
||||
dashboard.toggleRow(dashboard.panels[0]);
|
||||
dashboard.toggleRow(dashboard.panels[1]);
|
||||
expect(dashboard.panels.length).toBe(2);
|
||||
|
||||
// change variable
|
||||
dashboard.templating.list[1].current.value = ['se1', 'se2', 'se3'];
|
||||
|
||||
// toggle row back
|
||||
dashboard.toggleRow(dashboard.panels[1]);
|
||||
expect(dashboard.panels.length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,12 +43,23 @@ describe('ShareModalCtrl', function() {
|
||||
});
|
||||
|
||||
it('should generate render url', function() {
|
||||
ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/db/my-dash';
|
||||
ctx.$location.$$absUrl = 'http://dashboards.grafana.com/d/abcdefghi/my-dash';
|
||||
|
||||
ctx.scope.panel = { id: 22 };
|
||||
|
||||
ctx.scope.init();
|
||||
var base = 'http://dashboards.grafana.com/render/dashboard-solo/db/my-dash';
|
||||
var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash';
|
||||
var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
|
||||
expect(ctx.scope.imageUrl).to.contain(base + params);
|
||||
});
|
||||
|
||||
it('should generate render url for scripted dashboard', function() {
|
||||
ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/script/my-dash.js';
|
||||
|
||||
ctx.scope.panel = { id: 22 };
|
||||
|
||||
ctx.scope.init();
|
||||
var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js';
|
||||
var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
|
||||
expect(ctx.scope.imageUrl).to.contain(base + params);
|
||||
});
|
||||
|
||||
@@ -66,6 +66,11 @@ describe('unsavedChangesSrv', function() {
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it('Should ignore .iteration changes', () => {
|
||||
dash.iteration = new Date().getTime() + 1;
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
});
|
||||
|
||||
it.skip('Should ignore row collapse change', function() {
|
||||
dash.rows[0].collapse = true;
|
||||
expect(tracker.hasChanges()).to.be(false);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
const template = `
|
||||
|
||||
@@ -35,12 +35,12 @@ export class Tracker {
|
||||
|
||||
$window.onbeforeunload = () => {
|
||||
if (this.ignoreChanges()) {
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
if (this.hasChanges()) {
|
||||
return 'There are unsaved changes to this dashboard';
|
||||
}
|
||||
return '';
|
||||
return null;
|
||||
};
|
||||
|
||||
scope.$on('$locationChangeStart', (event, next) => {
|
||||
@@ -97,6 +97,9 @@ export class Tracker {
|
||||
dash.refresh = 0;
|
||||
dash.schemaVersion = 0;
|
||||
|
||||
// ignore iteration property
|
||||
delete dash.iteration;
|
||||
|
||||
// filter row and panels properties that should be ignored
|
||||
dash.rows = _.filter(dash.rows, function(row) {
|
||||
if (row.repeatRowId) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
const hitTypes = {
|
||||
FOLDER: 'dash-folder',
|
||||
DASHBOARD: 'dash-db',
|
||||
};
|
||||
|
||||
export class ValidationSrv {
|
||||
rootName = 'root';
|
||||
rootName = 'general';
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $q, private backendSrv) {}
|
||||
|
||||
validateNewDashboardOrFolderName(name) {
|
||||
validateNewDashboardName(folderId, name) {
|
||||
return this.validate(folderId, name, 'A dashboard in this folder with the same name already exists');
|
||||
}
|
||||
|
||||
validateNewFolderName(name) {
|
||||
return this.validate(0, name, 'A folder or dashboard in the general folder with the same name already exists');
|
||||
}
|
||||
|
||||
private validate(folderId, name, existingErrorMessage) {
|
||||
name = (name || '').trim();
|
||||
const nameLowerCased = name.toLowerCase();
|
||||
|
||||
if (name.length === 0) {
|
||||
return this.$q.reject({
|
||||
@@ -16,21 +30,35 @@ export class ValidationSrv {
|
||||
});
|
||||
}
|
||||
|
||||
if (name.toLowerCase() === this.rootName) {
|
||||
if (folderId === 0 && nameLowerCased === this.rootName) {
|
||||
return this.$q.reject({
|
||||
type: 'EXISTING',
|
||||
message: 'A folder or dashboard with the same name already exists',
|
||||
message: 'This is a reserved name and cannot be used for a folder.',
|
||||
});
|
||||
}
|
||||
|
||||
let deferred = this.$q.defer();
|
||||
|
||||
this.backendSrv.search({ query: name }).then(res => {
|
||||
for (let hit of res) {
|
||||
if (name.toLowerCase() === hit.title.toLowerCase()) {
|
||||
const promises = [];
|
||||
promises.push(this.backendSrv.search({ type: hitTypes.FOLDER, folderIds: [folderId], query: name }));
|
||||
promises.push(this.backendSrv.search({ type: hitTypes.DASHBOARD, folderIds: [folderId], query: name }));
|
||||
|
||||
this.$q.all(promises).then(res => {
|
||||
let hits = [];
|
||||
|
||||
if (res.length > 0 && res[0].length > 0) {
|
||||
hits = res[0];
|
||||
}
|
||||
|
||||
if (res.length > 1 && res[1].length > 0) {
|
||||
hits = hits.concat(res[1]);
|
||||
}
|
||||
|
||||
for (let hit of hits) {
|
||||
if (nameLowerCased === hit.title.toLowerCase()) {
|
||||
deferred.reject({
|
||||
type: 'EXISTING',
|
||||
message: 'A folder or dashboard with the same name already exists',
|
||||
message: existingErrorMessage,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ export class DashboardViewState {
|
||||
|
||||
this.dashboard.setViewMode(ctrl.panel, false, false);
|
||||
this.$scope.appEvent('panel-fullscreen-exit', { panelId: ctrl.panel.id });
|
||||
this.$scope.appEvent('dash-scroll', { restore: true });
|
||||
|
||||
if (!render) {
|
||||
return false;
|
||||
@@ -177,6 +178,7 @@ export class DashboardViewState {
|
||||
|
||||
this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
|
||||
this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id });
|
||||
this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
|
||||
}
|
||||
|
||||
registerPanel(panelScope) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="grafana-info-box">
|
||||
<h5>What are Dashboard Links?</h5>
|
||||
<p>
|
||||
Dashboad Links allow you to place links to other dashboards and web sites directly in below the dashboard header.
|
||||
Dashboard Links allow you to place links to other dashboards and web sites directly in below the dashboard header.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,9 @@ function dashLink($compile, linkSrv) {
|
||||
if (link.asDropdown) {
|
||||
template +=
|
||||
'<ul class="dropdown-menu" role="menu">' +
|
||||
'<li ng-repeat="dash in link.searchHits"><a href="{{dash.url}}">{{dash.title}}</a></li>' +
|
||||
'<li ng-repeat="dash in link.searchHits">' +
|
||||
'<a href="{{dash.url}}" target="{{dash.target}}">{{dash.title}}</a>' +
|
||||
'</li>' +
|
||||
'</ul>';
|
||||
}
|
||||
|
||||
@@ -84,6 +86,7 @@ export class DashLinksContainerCtrl {
|
||||
tags: linkDef.tags,
|
||||
keepTime: linkDef.keepTime,
|
||||
includeVars: linkDef.includeVars,
|
||||
target: linkDef.targetBlank ? '_blank' : '_self',
|
||||
icon: 'fa fa-bars',
|
||||
asDropdown: true,
|
||||
},
|
||||
@@ -128,6 +131,7 @@ export class DashLinksContainerCtrl {
|
||||
memo.push({
|
||||
title: dash.title,
|
||||
url: 'dashboard/' + dash.uri,
|
||||
target: link.target,
|
||||
icon: 'fa fa-th-large',
|
||||
keepTime: link.keepTime,
|
||||
includeVars: link.includeVars,
|
||||
|
||||
@@ -1,44 +1,26 @@
|
||||
<div class="container">
|
||||
<page-header model="navModel"></page-header>
|
||||
|
||||
<div class="signup-page-background">
|
||||
</div>
|
||||
<div class="page-container page-body">
|
||||
|
||||
<div class="login-content">
|
||||
|
||||
<div class="login-branding">
|
||||
<img src="img/logo_transparent_200x75.png">
|
||||
</div>
|
||||
|
||||
<div class="invite-box">
|
||||
<h3>
|
||||
<i class="fa fa-users"></i>
|
||||
Change active organization
|
||||
</h3>
|
||||
<div class="signup">
|
||||
<div class="login-form">
|
||||
|
||||
<div class="modal-tagline">
|
||||
You have been added to another Organization <br>
|
||||
due to an open invitation!
|
||||
<br><br>
|
||||
You have been added to another Organization due to an open invitation!
|
||||
|
||||
Please select which organization you want to <br>
|
||||
use right now (you can change this later at any time).
|
||||
</div>
|
||||
|
||||
<div style="display: inline-block; width: 400px; margin: 30px 0">
|
||||
<table class="filter-table">
|
||||
<tr ng-repeat="org in orgs">
|
||||
<td class="nobg max-width-btns">
|
||||
<a ng-click="setUsingOrg(org)" class="btn btn-inverse">
|
||||
{{org.name}} ({{org.role}})
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div ng-repeat="org in orgs">
|
||||
<a ng-click="setUsingOrg(org)" class="btn btn-success">
|
||||
{{org.name}} ({{org.role}})
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -26,11 +26,14 @@
|
||||
<div class="gf-form-group">
|
||||
|
||||
<h3 class="page-heading">Team Members</h3>
|
||||
|
||||
<form name="ctrl.addMemberForm" class="gf-form-group">
|
||||
<form name="ctrl.addMemberForm" class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Add member</span>
|
||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
||||
<!--
|
||||
Old picker
|
||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
||||
-->
|
||||
<select-user-picker class="width-7" handlePicked="ctrl.userPicked" backendSrv="ctrl.backendSrv"></select-user-picker>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -61,3 +64,4 @@
|
||||
This team has no members yet.
|
||||
</em>
|
||||
</div>
|
||||
|
||||
|
||||
2
public/app/features/org/partials/teams.html
Normal file → Executable file
2
public/app/features/org/partials/teams.html
Normal file → Executable file
@@ -8,7 +8,7 @@
|
||||
</label>
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
|
||||
<a class="btn btn-success" href="/org/teams/new">
|
||||
<a class="btn btn-success" href="org/teams/new">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add Team
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
|
||||
@@ -6,6 +6,14 @@ export class SelectOrgCtrl {
|
||||
constructor($scope, backendSrv, contextSrv) {
|
||||
contextSrv.sidemenu = false;
|
||||
|
||||
$scope.navModel = {
|
||||
main: {
|
||||
icon: 'gicon gicon-branding',
|
||||
subTitle: 'Preferences',
|
||||
text: 'Select active organization',
|
||||
},
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.getUserOrgs();
|
||||
};
|
||||
|
||||
@@ -8,6 +8,8 @@ export default class TeamDetailsCtrl {
|
||||
/** @ngInject **/
|
||||
constructor(private $scope, private backendSrv, private $routeParams, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'teams', 0);
|
||||
this.userPicked = this.userPicked.bind(this);
|
||||
this.get = this.get.bind(this);
|
||||
this.get();
|
||||
}
|
||||
|
||||
@@ -35,7 +37,7 @@ export default class TeamDetailsCtrl {
|
||||
}
|
||||
|
||||
removeMemberConfirmed(teamMember: TeamMember) {
|
||||
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get.bind(this));
|
||||
this.backendSrv.delete(`/api/teams/${this.$routeParams.id}/members/${teamMember.userId}`).then(this.get);
|
||||
}
|
||||
|
||||
update() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
|
||||
@@ -241,31 +241,10 @@ export class PanelCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
removePanel(ask: boolean) {
|
||||
// confirm deletion
|
||||
if (ask !== false) {
|
||||
var text2, confirmText;
|
||||
|
||||
if (this.panel.alert) {
|
||||
text2 = 'Panel includes an alert rule, removing panel will also remove alert rule';
|
||||
confirmText = 'YES';
|
||||
}
|
||||
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Remove Panel',
|
||||
text: 'Are you sure you want to remove this panel?',
|
||||
text2: text2,
|
||||
icon: 'fa-trash',
|
||||
confirmText: confirmText,
|
||||
yesText: 'Remove',
|
||||
onConfirm: () => {
|
||||
this.removePanel(false);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.dashboard.removePanel(this.panel);
|
||||
removePanel() {
|
||||
this.publishAppEvent('panel-remove', {
|
||||
panelId: this.panel.id,
|
||||
});
|
||||
}
|
||||
|
||||
editPanelJson() {
|
||||
|
||||
@@ -100,7 +100,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
|
||||
// update scrollbar after mounting
|
||||
ctrl.events.on('component-did-mount', () => {
|
||||
if (ctrl.__proto__.constructor.scrollable) {
|
||||
panelScrollbar = new PerfectScrollbar(panelContent[0]);
|
||||
panelScrollbar = new PerfectScrollbar(panelContent[0], {
|
||||
wheelPropagation: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
|
||||
var directiveModule = angular.module('grafana.directives');
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import $ from 'jquery';
|
||||
import { coreModule } from 'app/core/core';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export class QueryCtrl {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
|
||||
var module = angular.module('grafana.directives');
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { coreModule, JsonExplorer } from 'app/core/core';
|
||||
|
||||
@@ -1,19 +1,33 @@
|
||||
import angular from 'angular';
|
||||
import locationUtil from 'app/core/utils/location_util';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class SoloPanelCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv) {
|
||||
constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) {
|
||||
var panelId;
|
||||
|
||||
$scope.init = function() {
|
||||
contextSrv.sidemenu = false;
|
||||
appEvents.emit('toggle-sidemenu-hidden');
|
||||
|
||||
var params = $location.search();
|
||||
panelId = parseInt(params.panelId);
|
||||
|
||||
$scope.onAppEvent('dashboard-initialized', $scope.initPanelScope);
|
||||
|
||||
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) {
|
||||
// if no uid, redirect to new route based on slug
|
||||
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
|
||||
backendSrv.getDashboardBySlug($routeParams.slug).then(res => {
|
||||
if (res) {
|
||||
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
|
||||
$location.path(url).replace();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(function(result) {
|
||||
result.meta.soloMode = true;
|
||||
$scope.initDashboard(result, $scope);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from '../../core/core_module';
|
||||
|
||||
export class PlaylistSearchCtrl {
|
||||
@@ -14,6 +12,7 @@ export class PlaylistSearchCtrl {
|
||||
|
||||
$timeout(() => {
|
||||
this.query.query = '';
|
||||
this.query.type = 'dash-db';
|
||||
this.searchDashboards();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from '../../core/core_module';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import coreModule from '../../core/core_module';
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import './plugin_page_ctrl';
|
||||
import './plugin_list_ctrl';
|
||||
import './import_list/import_list';
|
||||
import './ds_edit_ctrl';
|
||||
import './ds_dashboards_ctrl';
|
||||
import './ds_list_ctrl';
|
||||
import './datasource_srv';
|
||||
import './plugin_component';
|
||||
|
||||
45
public/app/features/plugins/ds_dashboards_ctrl.ts
Normal file
45
public/app/features/plugins/ds_dashboards_ctrl.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { toJS } from 'mobx';
|
||||
import { coreModule } from 'app/core/core';
|
||||
import { store } from 'app/stores/store';
|
||||
|
||||
export class DataSourceDashboardsCtrl {
|
||||
datasourceMeta: any;
|
||||
navModel: any;
|
||||
current: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $routeParams) {
|
||||
if (store.nav.main === null) {
|
||||
store.nav.load('cfg', 'datasources');
|
||||
}
|
||||
|
||||
this.navModel = toJS(store.nav);
|
||||
|
||||
if (this.$routeParams.id) {
|
||||
this.getDatasourceById(this.$routeParams.id);
|
||||
}
|
||||
}
|
||||
|
||||
getDatasourceById(id) {
|
||||
this.backendSrv
|
||||
.get('/api/datasources/' + id)
|
||||
.then(ds => {
|
||||
this.current = ds;
|
||||
})
|
||||
.then(this.getPluginInfo.bind(this));
|
||||
}
|
||||
|
||||
updateNav() {
|
||||
store.nav.initDatasourceEditNav(this.current, this.datasourceMeta, 'datasource-dashboards');
|
||||
this.navModel = toJS(store.nav);
|
||||
}
|
||||
|
||||
getPluginInfo() {
|
||||
return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => {
|
||||
this.datasourceMeta = pluginInfo;
|
||||
this.updateNav();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('DataSourceDashboardsCtrl', DataSourceDashboardsCtrl);
|
||||
@@ -1,7 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import { toJS } from 'mobx';
|
||||
import config from 'app/core/config';
|
||||
import { coreModule, appEvents } from 'app/core/core';
|
||||
import { store } from 'app/stores/store';
|
||||
|
||||
var datasourceTypes = [];
|
||||
|
||||
@@ -23,24 +24,18 @@ export class DataSourceEditCtrl {
|
||||
types: any;
|
||||
testing: any;
|
||||
datasourceMeta: any;
|
||||
tabIndex: number;
|
||||
hasDashboards: boolean;
|
||||
editForm: any;
|
||||
gettingStarted: boolean;
|
||||
navModel: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private $q,
|
||||
private backendSrv,
|
||||
private $routeParams,
|
||||
private $location,
|
||||
private datasourceSrv,
|
||||
navModelSrv
|
||||
) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'datasources', 0);
|
||||
constructor(private $q, private backendSrv, private $routeParams, private $location, private datasourceSrv) {
|
||||
if (store.nav.main === null) {
|
||||
store.nav.load('cfg', 'datasources');
|
||||
}
|
||||
|
||||
this.navModel = toJS(store.nav);
|
||||
this.datasources = [];
|
||||
this.tabIndex = 0;
|
||||
|
||||
this.loadDatasourceTypes().then(() => {
|
||||
if (this.$routeParams.id) {
|
||||
@@ -55,8 +50,6 @@ export class DataSourceEditCtrl {
|
||||
this.isNew = true;
|
||||
this.current = _.cloneDeep(defaults);
|
||||
|
||||
this.navModel.breadcrumbs.push({ text: 'New' });
|
||||
|
||||
// We are coming from getting started
|
||||
if (this.$location.search().gettingstarted) {
|
||||
this.gettingStarted = true;
|
||||
@@ -82,12 +75,6 @@ export class DataSourceEditCtrl {
|
||||
this.backendSrv.get('/api/datasources/' + id).then(ds => {
|
||||
this.isNew = false;
|
||||
this.current = ds;
|
||||
this.navModel.node = {
|
||||
text: ds.name,
|
||||
icon: 'icon-gf icon-gf-fw icon-gf-datasources',
|
||||
id: 'ds-new',
|
||||
};
|
||||
this.navModel.breadcrumbs.push(this.navModel.node);
|
||||
|
||||
if (datasourceCreated) {
|
||||
datasourceCreated = false;
|
||||
@@ -112,11 +99,15 @@ export class DataSourceEditCtrl {
|
||||
this.typeChanged();
|
||||
}
|
||||
|
||||
updateNav() {
|
||||
store.nav.initDatasourceEditNav(this.current, this.datasourceMeta, 'datasource-settings');
|
||||
this.navModel = toJS(store.nav);
|
||||
}
|
||||
|
||||
typeChanged() {
|
||||
this.hasDashboards = false;
|
||||
return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => {
|
||||
this.datasourceMeta = pluginInfo;
|
||||
this.hasDashboards = _.find(pluginInfo.includes, { type: 'dashboard' }) !== undefined;
|
||||
this.updateNav();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,6 +162,7 @@ export class DataSourceEditCtrl {
|
||||
if (this.current.id) {
|
||||
return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(result => {
|
||||
this.current = result.datasource;
|
||||
this.updateNav();
|
||||
this.updateFrontendSettings().then(() => {
|
||||
this.testDatasource();
|
||||
});
|
||||
|
||||
@@ -6,12 +6,10 @@
|
||||
<i class="icon-gf icon-gf-dashboard"></i>
|
||||
</td>
|
||||
<td>
|
||||
<a href="dashboard/{{dash.importedUri}}" ng-show="dash.imported">
|
||||
<a href="{{dash.importedUrl}}" ng-show="dash.imported">
|
||||
{{dash.title}}
|
||||
</a>
|
||||
<span ng-show="!dash.imported">
|
||||
{{dash.title}}
|
||||
</span>
|
||||
<span ng-show="!dash.imported">{{dash.title}}</span>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
<button class="btn btn-secondary btn-small" ng-click="ctrl.import(dash, false)" ng-show="!dash.imported">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
7
public/app/features/plugins/partials/ds_dashboards.html
Normal file
7
public/app/features/plugins/partials/ds_dashboards.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body" ng-if="ctrl.datasourceMeta">
|
||||
|
||||
<dashboard-import-list plugin="ctrl.datasourceMeta" datasource="ctrl.current"></dashboard-import-list>
|
||||
|
||||
</div>
|
||||
@@ -8,9 +8,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="page-sub-heading" ng-hide="ctrl.isNew">Edit Data Source</h3>
|
||||
<h3 class="page-sub-heading" ng-show="ctrl.isNew">New Data Source</h3>
|
||||
|
||||
<form name="ctrl.editForm" ng-if="ctrl.current">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<empty-list-cta model="{
|
||||
title: 'There are no data sources defined yet',
|
||||
buttonIcon: 'gicon gicon-add-datasources',
|
||||
buttonLink: '/datasources/new',
|
||||
buttonLink: 'datasources/new',
|
||||
buttonTitle: 'Add data source',
|
||||
proTip: 'You can also define data sources through configuration files.',
|
||||
proTipLink: 'http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list',
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import Remarkable from 'remarkable';
|
||||
@@ -34,8 +32,8 @@ export class PluginEditCtrl {
|
||||
img: model.info.logos.large,
|
||||
subTitle: model.info.author.name,
|
||||
url: '',
|
||||
text: '',
|
||||
breadcrumbs: [{ title: 'Plugins', url: '/plugins' }, { title: model.name }],
|
||||
text: model.name,
|
||||
breadcrumbs: [{ title: 'Plugins', url: 'plugins' }],
|
||||
children: [
|
||||
{
|
||||
icon: 'fa fa-fw fa-file-text-o',
|
||||
|
||||
@@ -40,8 +40,8 @@ export class AppPageCtrl {
|
||||
img: app.info.logos.large,
|
||||
subTitle: app.name,
|
||||
url: '',
|
||||
text: '',
|
||||
breadcrumbs: [{ title: app.name, url: pluginNav.main.url }, { title: this.page.name }],
|
||||
text: this.page.name,
|
||||
breadcrumbs: [{ title: app.name, url: pluginNav.main.url }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
export class DashboardRowCtrl {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
|
||||
@@ -14,14 +14,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane style-guide-icon-list">
|
||||
<div class="row">
|
||||
<div ng-repeat="icon in ctrl.icons" class="col-md-2 col-sm-3 col-xs-4">
|
||||
<i class="icon-gf icon-gf-{{icon}}" bs-tooltip="'icon-gf icon-gf-{{icon}}'"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="page-heading">Forms</h3>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
|
||||
@@ -1,51 +1,17 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
|
||||
class StyleGuideCtrl {
|
||||
colors: any = [];
|
||||
theme: string;
|
||||
buttonNames = ['primary', 'secondary', 'inverse', 'success', 'warning', 'danger'];
|
||||
buttonSizes = ['btn-small', '', 'btn-large'];
|
||||
buttonVariants = ['-'];
|
||||
icons: any = [];
|
||||
page: any;
|
||||
pages = ['colors', 'buttons', 'icons', 'plugins'];
|
||||
navModel: any;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private $http, private $routeParams, private backendSrv, navModelSrv) {
|
||||
constructor(private $routeParams, private backendSrv, navModelSrv) {
|
||||
this.navModel = navModelSrv.getNav('cfg', 'admin', 'styleguide', 1);
|
||||
this.theme = config.bootData.user.lightTheme ? 'light' : 'dark';
|
||||
this.page = {};
|
||||
|
||||
if ($routeParams.page) {
|
||||
this.page[$routeParams.page] = 1;
|
||||
} else {
|
||||
this.page.colors = true;
|
||||
}
|
||||
|
||||
if (this.page.colors) {
|
||||
this.loadColors();
|
||||
}
|
||||
|
||||
if (this.page.icons) {
|
||||
this.loadIcons();
|
||||
}
|
||||
}
|
||||
|
||||
loadColors() {
|
||||
this.$http.get('public/build/styleguide.json').then(res => {
|
||||
this.colors = _.map(res.data[this.theme], (value, key) => {
|
||||
return { name: key, value: value };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadIcons() {
|
||||
this.$http.get('public/sass/icons.json').then(res => {
|
||||
this.icons = res.data;
|
||||
});
|
||||
}
|
||||
|
||||
switchTheme() {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import { Variable, assignModelProperties, variableTypes } from './variable';
|
||||
|
||||
export class ConstantVariable implements Variable {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import { Variable, assignModelProperties, variableTypes } from './variable';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { Variable, containsVariable, assignModelProperties, variableTypes } from './variable';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { variableTypes } from './variable';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class VariableEditorCtrl {
|
||||
/** @ngInject **/
|
||||
@@ -56,16 +57,13 @@ export class VariableEditorCtrl {
|
||||
}
|
||||
|
||||
if (!$scope.current.name.match(/^\w+$/)) {
|
||||
$scope.appEvent('alert-warning', [
|
||||
'Validation',
|
||||
'Only word and digit characters are allowed in variable names',
|
||||
]);
|
||||
appEvents.emit('alert-warning', ['Validation', 'Only word and digit characters are allowed in variable names']);
|
||||
return false;
|
||||
}
|
||||
|
||||
var sameName = _.find($scope.variables, { name: $scope.current.name });
|
||||
if (sameName && sameName !== $scope.current) {
|
||||
$scope.appEvent('alert-warning', ['Validation', 'Variable with the same name already exists']);
|
||||
appEvents.emit('alert-warning', ['Validation', 'Variable with the same name already exists']);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -73,7 +71,7 @@ export class VariableEditorCtrl {
|
||||
$scope.current.type === 'query' &&
|
||||
$scope.current.query.match(new RegExp('\\$' + $scope.current.name + '(/| |$)'))
|
||||
) {
|
||||
$scope.appEvent('alert-warning', [
|
||||
appEvents.emit('alert-warning', [
|
||||
'Validation',
|
||||
'Query cannot contain a reference to itself. Variable: $' + $scope.current.name,
|
||||
]);
|
||||
@@ -96,11 +94,11 @@ export class VariableEditorCtrl {
|
||||
};
|
||||
|
||||
$scope.runQuery = function() {
|
||||
return variableSrv.updateOptions($scope.current).then(null, function(err) {
|
||||
return variableSrv.updateOptions($scope.current).catch(err => {
|
||||
if (err.data && err.data.message) {
|
||||
err.message = err.data.message;
|
||||
}
|
||||
$scope.appEvent('alert-error', ['Templating', 'Template variables could not be initialized: ' + err.message]);
|
||||
appEvents.emit('alert-error', ['Templating', 'Template variables could not be initialized: ' + err.message]);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { Variable, assignModelProperties, variableTypes } from './variable';
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
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
|
||||
<h5>What do variables do?</h5>
|
||||
<p>Variables enable 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.
|
||||
|
||||
Checkout the
|
||||
Check out the
|
||||
<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
|
||||
Templating documentation
|
||||
</a> for more information.
|
||||
@@ -93,7 +93,7 @@
|
||||
</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>
|
||||
<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__', that's reserved for Grafana's global variables</span>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
|
||||
40
public/app/features/templating/specs/editor_ctrl.jest.ts
Normal file
40
public/app/features/templating/specs/editor_ctrl.jest.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { VariableEditorCtrl } from '../editor_ctrl';
|
||||
|
||||
let mockEmit;
|
||||
jest.mock('app/core/app_events', () => {
|
||||
mockEmit = jest.fn();
|
||||
return {
|
||||
emit: mockEmit,
|
||||
};
|
||||
});
|
||||
|
||||
describe('VariableEditorCtrl', () => {
|
||||
let scope = {
|
||||
runQuery: () => {
|
||||
return Promise.resolve({});
|
||||
},
|
||||
};
|
||||
|
||||
describe('When running a variable query and the data source returns an error', () => {
|
||||
beforeEach(() => {
|
||||
const variableSrv = {
|
||||
updateOptions: () => {
|
||||
return Promise.reject({
|
||||
data: { message: 'error' },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return new VariableEditorCtrl(scope, {}, variableSrv, {});
|
||||
});
|
||||
|
||||
it('should emit an error', () => {
|
||||
return scope.runQuery().then(res => {
|
||||
expect(mockEmit).toBeCalled();
|
||||
expect(mockEmit.mock.calls[0][0]).toBe('alert-error');
|
||||
expect(mockEmit.mock.calls[0][1][0]).toBe('Templating');
|
||||
expect(mockEmit.mock.calls[0][1][1]).toBe('Template variables could not be initialized: error');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -31,12 +31,40 @@ describe('templateSrv', function() {
|
||||
expect(target).toBe('this.mupp.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test} with scoped value', function() {
|
||||
var target = _templateSrv.replace('this.${test}.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.mupp.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with scoped value', function() {
|
||||
var target = _templateSrv.replace('this.${test:glob}.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.mupp.filters');
|
||||
});
|
||||
|
||||
it('should replace $test with scoped text', function() {
|
||||
var target = _templateSrv.replaceWithText('this.$test.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.asd.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test} with scoped text', function() {
|
||||
var target = _templateSrv.replaceWithText('this.${test}.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.asd.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with scoped text', function() {
|
||||
var target = _templateSrv.replaceWithText('this.${test:glob}.filters', {
|
||||
test: { value: 'mupp', text: 'asd' },
|
||||
});
|
||||
expect(target).toBe('this.asd.filters');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAdhocFilters', function() {
|
||||
@@ -84,13 +112,28 @@ describe('templateSrv', function() {
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test} with globbed value', function() {
|
||||
var target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with globbed value', function() {
|
||||
var target = _templateSrv.replace('this.${test:glob}.filters', {});
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace $test with piped value', function() {
|
||||
var target = _templateSrv.replace('this=$test', {}, 'pipe');
|
||||
expect(target).toBe('this=value1|value2');
|
||||
});
|
||||
|
||||
it('should replace $test with piped value', function() {
|
||||
var target = _templateSrv.replace('this=$test', {}, 'pipe');
|
||||
it('should replace ${test} with piped value', function() {
|
||||
var target = _templateSrv.replace('this=${test}', {}, 'pipe');
|
||||
expect(target).toBe('this=value1|value2');
|
||||
});
|
||||
|
||||
it('should replace ${test:pipe} with piped value', function() {
|
||||
var target = _templateSrv.replace('this=${test:pipe}', {});
|
||||
expect(target).toBe('this=value1|value2');
|
||||
});
|
||||
});
|
||||
@@ -111,6 +154,16 @@ describe('templateSrv', function() {
|
||||
var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test} with formatted all value', function() {
|
||||
var target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with formatted all value', function() {
|
||||
var target = _templateSrv.replace('this.${test:glob}.filters', {});
|
||||
expect(target).toBe('this.{value1,value2}.filters');
|
||||
});
|
||||
});
|
||||
|
||||
describe('variable with all option and custom value', function() {
|
||||
@@ -131,6 +184,16 @@ describe('templateSrv', function() {
|
||||
expect(target).toBe('this.*.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test} with formatted all value', function() {
|
||||
var target = _templateSrv.replace('this.${test}.filters', {}, 'glob');
|
||||
expect(target).toBe('this.*.filters');
|
||||
});
|
||||
|
||||
it('should replace ${test:glob} with formatted all value', function() {
|
||||
var target = _templateSrv.replace('this.${test:glob}.filters', {});
|
||||
expect(target).toBe('this.*.filters');
|
||||
});
|
||||
|
||||
it('should not escape custom all value', function() {
|
||||
var target = _templateSrv.replace('this.$test', {}, 'regex');
|
||||
expect(target).toBe('this.*');
|
||||
@@ -143,6 +206,18 @@ describe('templateSrv', function() {
|
||||
var target = _templateSrv.replace('this:$test', {}, 'lucene');
|
||||
expect(target).toBe('this:value\\/4');
|
||||
});
|
||||
|
||||
it('should properly escape ${test} with lucene escape sequences', function() {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
var target = _templateSrv.replace('this:${test}', {}, 'lucene');
|
||||
expect(target).toBe('this:value\\/4');
|
||||
});
|
||||
|
||||
it('should properly escape ${test:lucene} with lucene escape sequences', function() {
|
||||
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]);
|
||||
var target = _templateSrv.replace('this:${test:lucene}', {});
|
||||
expect(target).toBe('this:value\\/4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('format variable to string values', function() {
|
||||
@@ -185,6 +260,11 @@ describe('templateSrv', function() {
|
||||
expect(result).toBe('test');
|
||||
});
|
||||
|
||||
it('multi value and csv format should render csv string', function() {
|
||||
var result = _templateSrv.formatValue(['test', 'test2'], 'csv');
|
||||
expect(result).toBe('test,test2');
|
||||
});
|
||||
|
||||
it('slash should be properly escaped in regex format', function() {
|
||||
var result = _templateSrv.formatValue('Gi3/14', 'regex');
|
||||
expect(result).toBe('Gi3\\/14');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user