ui: backport UI improvements

This commit is contained in:
Fabian Reinartz 2016-09-07 17:56:31 +02:00
parent 8dd8ef08f7
commit 21f0299bc5
13 changed files with 487 additions and 284 deletions

File diff suppressed because one or more lines are too long

View File

@ -93,17 +93,17 @@ a.gen-link button {
opacity: 1.0;
}
.active-silences, .pending-silences, .elapsed-silences {
.active-silences, .elapsed-silences {
margin-bottom: 12px;
}
.alert-item .overview {
background: #f0f0f0;
padding: .8em;
padding: .8em 0;
}
.silence-item .overview {
background: #bfbfbf;
padding: .8em;
padding: .8em 0;
}
.silence-item.highlight .overview {
background: #dfdfdf;
@ -145,8 +145,8 @@ a.gen-link button {
.lbl {
display: inline-block;
font-size: 0.7em;
padding: 0 6px;
font-size: 0.8em;
padding: 4px 6px;
margin: 0 2px 2px 0;
font-family: Menlo, Monaco, Consolas, sans-serif;
border: 1px solid #ccc;
@ -232,6 +232,7 @@ input[type="datetime-local"] {
border-radius: 2px;
margin-bottom: 0;
color: rgba(0, 0, 0, 0.85);
width: 100%;
}
/* Routing Tree */
@ -275,3 +276,14 @@ input[type="datetime-local"] {
display: inline-block;
}
.pointer {
cursor: pointer;
}
.pagination-height {
height: 30px;
}
.btn-spacing {
margin: 2px;
}

View File

@ -9,11 +9,13 @@
<script src="lib/angular-route.min.js"></script>
<script src="lib/angular-resource.min.js"></script>
<script src="lib/angular-moment.min.js"></script>
<script src="lib/ui-bootstrap-custom-tpls-2.0.1.min.js"></script>
<script src="app/js/app.js"></script>
<link rel="stylesheet" href="lib/kube.min.css">
<link rel="stylesheet" href="app/css/main.css">
<link rel="stylesheet" href="lib/bootstrap.min.css">
<title>AlertManager Prometheus</title>
</head>

View File

@ -49,7 +49,10 @@ angular.module('am.directives').directive('silenceForm',
return {
restrict: 'E',
scope: {
silence: '='
silence: '=',
formTitle: '@',
formSubtitle: '@',
buttonText: '@'
},
templateUrl: 'app/partials/silence-form.html'
};
@ -263,129 +266,180 @@ angular.module('am.controllers').controller('SilenceCtrl',
}
);
angular.module('am.controllers').controller('SilencesCtrl',
function($scope, Silence) {
$scope.silences = [];
$scope.order = "endsAt";
angular.module('am.controllers').controller('SilencesCtrl', function($scope, $location, Silence, totalSilences) {
$scope.totalItems = totalSilences;
var DEFAULT_PER_PAGE = 25;
var DEFAULT_PAGE = 1;
$scope.silences = [];
$scope.showForm = false;
$scope.showForm = false;
$scope.toggleForm = function() {
$scope.showForm = !$scope.showForm
}
$scope.refresh = function() {
Silence.query({},
function(data) {
$scope.silences = data.data || [];
var now = new Date;
angular.forEach($scope.silences, function(value) {
value.endsAt = new Date(value.endsAt);
value.startsAt = new Date(value.startsAt);
value.updatedAt = new Date(value.updatedAt);
value.elapsed = value.endsAt < now;
value.pending = value.startsAt > now;
value.active = value.startsAt <= now && value.endsAt > now;
});
},
function(data) {
$scope.error = data.data;
}
);
};
$scope.$on('silence-created', function(evt) {
$scope.refresh();
});
$scope.$on('silence-deleted', function(evt) {
$scope.refresh();
});
$scope.refresh();
$scope.toggleForm = function() {
$scope.showForm = !$scope.showForm;
}
);
angular.module('am.controllers').controller('SilenceCreateCtrl',
function($scope, Silence) {
$scope.error = null;
$scope.silence = $scope.silence || {};
// Pagination
$scope.pageChanged = function() {
$location.search('page', $scope.currentPage);
};
if (!$scope.silence.matchers) {
$scope.silence.matchers = [{}];
var params = $location.search()
var page = params['page'];
if (!page) {
$location.search('page', DEFAULT_PAGE);
}
$scope.currentPage = parseInt($location.search()['page']);
$scope.itemsPerPage = params['limit'];
if (isNaN(parseInt($scope.itemsPerPage))) {
$scope.itemsPerPage = DEFAULT_PER_PAGE;
// $location.search('limit', $scope.itemsPerPage);
}
$scope.setPerPage = function(n) {
$scope.itemsPerPage = n;
$location.search('limit', $scope.itemsPerPage);
};
// Controls the number of pages to display in the pagination list.
$scope.maxSize = 8;
// Arbitrary suggested page lengths. The user can override this at any time
// by entering their own n value in the url.
$scope.paginationLengths = [5,15,25,50];
// End Pagination
$scope.refresh = function() {
var search = $location.search();
var params = {};
if (search['page']) {
params['offset'] = search['page']-1;
} else {
params['offset'] = DEFAULT_PAGE-1;
}
$scope.currentPage = params['offset']+1;
if (search['limit']) {
params['limit'] = search['limit'];
$scope.itemsPerPage = params['limit'];
} else {
params['limit'] = $scope.itemsPerPage;
// $scope.setPerPage(params['limit']);
}
var origSilence = angular.copy($scope.silence);
$scope.reset = function() {
var now = new Date();
var end = new Date();
now.setMilliseconds(0);
end.setMilliseconds(0);
now.setSeconds(0);
end.setSeconds(0);
end.setHours(end.getHours() + 4)
$scope.silence = angular.copy(origSilence);
if (!origSilence.startsAt || origSilence.elapsed) {
$scope.silence.startsAt = now;
}
if (!origSilence.endsAt || origSilence.elapsed) {
$scope.silence.endsAt = end;
}
};
$scope.reset();
$scope.addMatcher = function() {
$scope.silence.matchers.push({});
};
$scope.delMatcher = function(i) {
$scope.silence.matchers.splice(i, 1);
};
$scope.create = function() {
Silence.query(params, function(resp) {
var data = resp.data;
$scope.silences = data.silences || [];
$scope.totalItems = data.totalSilences;
var now = new Date;
// Go through conditions that go against immutability of historic silences.
var createNew = !angular.equals(origSilence.matchers, $scope.silence.matchers);
console.log(origSilence, $scope.silence);
createNew = createNew || $scope.silence.elapsed;
createNew = createNew || ($scope.silence.active && (origSilence.startsAt == $scope.silence.startsAt || origSilence.endsAt == $scope.silence.endsAt));
if (createNew) {
$scope.silence.id = undefined;
angular.forEach($scope.silences, function(value) {
value.endsAt = new Date(value.endsAt);
value.startsAt = new Date(value.startsAt);
value.updatedAt = new Date(value.updatedAt);
value.elapsed = value.endsAt < now;
value.pending = value.startsAt > now;
value.active = value.startsAt <= now && value.endsAt > now;
});
}, function(data) {
$scope.error = data.data;
});
};
$scope.$on('silence-created', function(evt) {
$scope.toggleForm();
$scope.refresh();
});
$scope.$on('silence-deleted', function(evt) {
$scope.refresh();
});
$scope.elapsed = function(elapsed) {
return function(sil) {
if (elapsed) {
return sil.endsAt <= new Date;
}
return sil.endsAt > new Date;
}
};
Silence.create($scope.silence,
function(data) {
// If the modifications require creating a new silence,
// we expire/delete the old one.
if (createNew && origSilence.id && !$scope.silence.elapsed) {
Silence.delete({id: origSilence.id},
function(data) {
// Only trigger reload after after old silence was deleted.
$scope.$emit('silence-created');
},
function(data) {
console.warn("deleting silence failed", data);
$scope.$emit('silence-created');
});
} else {
$scope.$emit('silence-created');
}
},
function(data) {
$scope.error = data.data.error;
}
);
};
$scope.$watch(function() {
return $location.search();
}, function() {
$scope.refresh();
}, true);
});
angular.module('am.controllers').controller('SilenceCreateCtrl', function($scope, Silence) {
$scope.error = null;
$scope.silence = $scope.silence || {};
if (!$scope.silence.matchers) {
$scope.silence.matchers = [{}];
}
);
var origSilence = angular.copy($scope.silence);
$scope.reset = function() {
var now = new Date();
var end = new Date();
now.setMilliseconds(0);
end.setMilliseconds(0);
now.setSeconds(0);
end.setSeconds(0);
end.setHours(end.getHours() + 4)
$scope.silence = angular.copy(origSilence);
if (!origSilence.startsAt || origSilence.elapsed) {
$scope.silence.startsAt = now;
}
if (!origSilence.endsAt || origSilence.elapsed) {
$scope.silence.endsAt = end;
}
};
$scope.reset();
$scope.$on('silence-created', function(evt) {
$scope.form.$setUntouched();
$scope.reset();
});
$scope.addMatcher = function() {
$scope.silence.matchers.push({});
};
$scope.delMatcher = function(i) {
$scope.silence.matchers.splice(i, 1);
};
$scope.create = function() {
if (origSilence.id) {
$scope.silence.id = undefined;
}
Silence.create($scope.silence, function(data) {
if (origSilence.id) {
Silence.delete({id: origSilence.id},
function(data) {
// Only trigger reload after after old silence was deleted.
$scope.$emit('silence-created');
},
function(data) {
console.warn("deleting silence failed", data);
$scope.$emit('silence-created');
});
} else {
$scope.$emit('silence-created');
}
}, function(data) {
$scope.error = data.data.error;
});
};
});
angular.module('am.services').factory('Status',
function($resource) {
@ -407,7 +461,7 @@ angular.module('am.controllers').controller('StatusCtrl',
$scope.uptime = data.data.uptime;
},
function(data) {
console.log(data.data);
console.log(data.data);
})
}
);
@ -416,6 +470,7 @@ angular.module('am', [
'ngRoute',
'ngSanitize',
'angularMoment',
'ui.bootstrap',
'am.controllers',
'am.services',
@ -433,6 +488,18 @@ angular.module('am').config(
when('/silences', {
templateUrl: 'app/partials/silences.html',
controller: 'SilencesCtrl',
resolve: {
totalSilences: function($q, Silence) {
// Required to get the total number of silences before the controller
// loads. Without this, the user is forced to page 1 of the
// pagination.
var defer = $q.defer();
Silence.query({'limit':0}, function(resp) {
defer.resolve(resp.data.totalSilences);
});
return defer.promise;
}
},
reloadOnSearch: false
}).
when('/status', {

View File

@ -17,14 +17,14 @@
<div class="right">
<div ng-show="alert.inhibited" class="left lbl muted-lbl">inhibited</div>
<div ng-show="alert.silenced" class="left lbl muted-lbl"><a ng-href="/#/silences?hl={{ alert.silenced }}">silenced</a></div>
<button type="black" disabled small>Since {{ alert.startsAt | amCalendar }}</button>
<button class="silence-button" type="black" ng-click="toggleSilenceForm()" small upper>Silence</button>
</div>
</div>
<div class="silence-alert" ng-show="showSilenceForm">
<silence-form silence="silence"></silence-form>
<silence-form silence="silence" form-title="Silence" form-subtitle="Stop sending notifications for this alert" button-text="Create"></silence-form>
</div>
<div class="detail group" ng-show="showDetails">
@ -37,4 +37,4 @@
</tbody>
</table>
</div>
</div>
</div>

View File

@ -1,55 +1,84 @@
<form novalidate name="form" class="forms" ng-controller="SilenceCreateCtrl">
<fieldset id="silence-create">
<legend>Create <span class="desc">Define a new silence.</span></legend>
<div class="row">
<div class="col-sm-12">
<h2>{{formTitle}} <small>{{formSubtitle}}</small></h2>
</div>
</div>
<row>
<column>
<label>Start</label>
<input ng-model="silence.startsAt" type="datetime-local" name="start-time" required>
</column>
<column>
<label>End</label>
<input ng-model="silence.endsAt" type="datetime-local" name="end-time" required>
</column>
</row>
<label>Matchers <span class="desc">Alerts affected by this silence.</span></label>
<row class="silence-matchers" ng-repeat="m in silence.matchers">
<column cols="2">
<input class="input-small" type="text" placeholder="name" ng-model="m.name" required>
</column>
<column cols="2">
<input class="input-small" type="text" placeholder="value" ng-model="m.value" required>
</column>
<div class="row" class="silence-matchers">
<div class="col-sm-3">
<label>Start</label>
<div class="form-group">
<label class="sr-only" for="silence-start-time">Name</label>
<input id="silence-start-time" ng-model="silence.startsAt" type="datetime-local" name="start-time" required>
</div>
</div>
<div class="col-sm-3">
<label>End</label>
<div class="form-group">
<label class="sr-only" for="silence-end-time">Name</label>
<input id="silence-end-time" ng-model="silence.endsAt" type="datetime-local" name="end-time" required>
</div>
</div>
</div>
<column>
<div class="btn-group">
<button type="primary" small><label class="checkbox is-regex"><input type="checkbox" ng-model="m.isRegex"> regex</label></button>
<button type="secondary" ng-hide="silence.matchers.length <= 1" ng-click="delMatcher($index)" small>-</button>
<button type="secondary" ng-click="addMatcher()" small>+</button>
</div>
</column>
</row>
<label>Matchers <span class="desc">Alerts affected by this silence.</span></label>
<div ng-repeat="m in silence.matchers">
<div class="row" class="silence-matchers">
<div class="col-sm-3">
<div class="form-group">
<label class="sr-only" for="silence-name-input">Name</label>
<input type="text" class="form-control" id="silence-name-input" placeholder="Name" ng-model="m.name" required>
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<label class="sr-only" for="silence-value-input">Value</label>
<input type="text" class="form-control" id="silence-value-input" placeholder="Value" ng-model="m.value" reguired>
</div>
</div>
<div class="col-sm-1">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="m.isRegex"> Regex
</label>
</div>
</div>
<div class="col-sm-1" ng-if="!$last">
<button type="button" class="btn btn-danger btn-sm" ng-if="silence.matchers.length > 1" ng-click="delMatcher($index)">-</button>
</div>
<div class="col-sm-1 btn-group" ng-if="$last">
<button type="button" class="btn btn-danger btn-sm" ng-if="silence.matchers.length > 1" ng-click="delMatcher($index)">-</button>
<button type="button" class="btn btn-primary btn-sm" ng-click="addMatcher()">+</button>
</div>
</div>
</div>
<row>
<column cols="2">
<label>Creator</label>
<input ng-model="silence.createdBy" type="email" name="creator" placeholder="me@company.com" required>
</column>
<column cols="4">
<label>Comment</label>
<input ng-model="silence.comment" type="text" name="comment" placeholder="reason for silence..." required>
</column>
</row>
<div ng-show="error != null" class="alert alert-error">
<span class="error">{{ error }}</span>
</div>
<div class="row">
<div class="col-sm-3">
<label>Creator</label>
<div class="form-group">
<label class="sr-only" for="silence-created-by">Creator</label>
<input id="silence-created-by" ng-model="silence.createdBy" type="email" name="creator" placeholder="me@company.com" required>
</div>
</div>
<div class="col-sm-5">
<label>Comment</label>
<div class="form-group">
<label class="sr-only" for="silence-created-reason">Name</label>
<input id="silence-created-reason" ng-model="silence.comment" type="text" name="comment" placeholder="Reason for silence..." required>
</div>
</div>
</div>
</fieldset>
<div ng-show="error != null" class="alert alert-error">
<span class="error">{{ error }}</span>
</div>
<div class="btn-group">
<button type="primary" ng-disabled="silence.matchers.length == 0 || form.$invalid" ng-click="create()" upper>Create</button>
<button type="seconday" ng-click="reset()" upper>Reset</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary" ng-disabled="silence.matchers.length == 0 || form.$invalid" ng-click="create()" upper>{{buttonText}}</button>
<button type="button" class="btn btn-danger" ng-click="reset()" upper>Reset</button>
</div>
</form>

View File

@ -1,28 +1,26 @@
<div class="silence-item {{ highlight ? 'highlight' : ''}}" ng-controller="SilenceCtrl">
<div class="overview group">
<button type="primary" class="expand" ng-show="showDetails" ng-click="toggleDetails()" small></button>
<button type="primary" class="expand" ng-hide="showDetails" ng-click="toggleDetails()" small>+</button>
<div class="labels left">
<span ng-repeat="m in sil.matchers | orderBy:name">
<span class="lbl {{ m.name == 'alertname' ? 'lbl-highlight' : '' }}">
{{ m.name }} =<span ng-show="m.isRegex">~</span> "{{ m.value }}"
</span>
</span>
</div>
<div class="col-lg-8">
<button type="button" class="btn btn-primary btn-sm btn-spacing pagination-height" ng-show="showDetails" ng-click="toggleDetails()"></button>
<button type="button" class="btn btn-primary btn-sm btn-spacing pagination-height" ng-hide="showDetails" ng-click="toggleDetails()">+</button>
<button type="button" ng-repeat="m in sil.matchers | orderBy:name" ng-class="{'btn-danger': m.name == 'alertname'}" class="btn btn-default btn-xs btn-spacing pagination-height">
{{ m.name }} =<span ng-show="m.isRegex">~</span> "{{ m.value }}"
</button>
</div>
<div class="col-lg-4">
<div class="pull-right">
<button type="black" disabled small>Until {{ sil.endsAt | amCalendar }}</button>
<button class="delete-button" type="black" ng-click="delete(sil.id)" small upper>Delete</button>
<button class="edit-button" type="black" ng-click="toggleSilenceForm()" small upper>Edit</button>
</div>
</div>
<div class="right">
<button ng-show="sil.pending" type="black" disabled small>Starts {{ sil.endsAt | amCalendar }}</button>
<button ng-show="sil.active" type="black" disabled small>Ends {{ sil.endsAt | amCalendar }}</button>
<button ng-show="sil.pending" class="delete-button" type="black" ng-click="delete(sil.id)" small upper>Delete</button>
<button ng-show="sil.active" class="delete-button" type="black" ng-click="delete(sil.id)" small upper>Expire</button>
<button ng-show="!sil.elapsed" class="edit-button" type="black" ng-click="toggleSilenceForm()" small upper>Edit</button>
<button ng-show="sil.elapsed" class="edit-button" type="black" ng-click="toggleSilenceForm()" small upper>Recreate</button>
</div>
</div>
<div class="edit-silence" ng-show="showSilenceForm">
<silence-form silence="sil"></silence-form>
<silence-form silence="sil" form-title="Edit" button-text="Update"></silence-form>
</div>
<div class="detail group" ng-show="showDetails">
@ -40,7 +38,7 @@
<td>active</td>
<td>
<span>{{ sil.startsAt | date:'yyyy-MM-dd HH:mm' }}</span>
<span>{{ sil.endsAt | date:'yyyy-MM-dd HH:mm' }}</span>
</td>
</tr>

View File

@ -1,28 +1,42 @@
<div class="group">
<div class="right">
<button ng-hide="showForm" type="primary" ng-click="toggleForm()" small>New Silence</button>
<button ng-show="showForm" type="primary" ng-click="toggleForm()" small>Hide Form</button>
</div>
<div ng-show="showForm">
<silence-form sil="silence"></silence-form>
</div>
<div class="row">
<div ng-show="showForm" class="col-sm-11">
<silence-form sil="silence" form-title="Create" form-subtitle="Define a new silence" button-text="Create"></silence-form>
</div>
<div class="col-sm-1">
<button ng-show="showForm" type="button" class="btn btn-primary btn-sm" ng-click="toggleForm()">Hide Form</button>
</div>
</div>
<div id="silences-query" class="forms">
<row>
<column cols="5">
<input type="search" placeholder="search" ng-model="query">
</column>
<column cols="2">
<select class="select" ng-model="order">
<option value="startsAt">start</option>
<option value="endsAt">end</option>
<option value="createdAt">created</option>
</select>
</column>
</row>
<div id="silences-query" class="form" ng-if="!showForm">
<div class="row">
<div class="col-sm-11">
<h2>Search</h2>
</div>
<div class="col-sm-1">
<button ng-hide="showForm" type="button" class="btn btn-primary btn-sm" ng-click="toggleForm()">New Silence</button>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<label class="sr-only">Search</label>
<input class="form-control" placeholder="Search" ng-model="query">
</div>
</div>
</div>
<div uib-dropdown dropdown-append-to-body ng-if="false">
<button type="button" class="btn btn-default btn-sm pagination-height" uib-dropdown-toggle>
{{itemsPerPage}} <span class="caret"></span>
</button>
<ul class="dropdown-menu" uib-dropdown-menu role="menu" aria-labelledby="btn-append-to-body">
<li ng-click="setPerPage(l)" role="menuitem" ng-repeat="l in paginationLengths">
<a class="pointer">{{l}}</a>
</li>
</ul>
</div>
<ul uib-pagination total-items="totalItems" items-per-page="itemsPerPage" ng-model="currentPage" ng-change="pageChanged()" max-size="maxSize" class="pagination-sm" boundary-link-numbers="true" rotate="false"></ul>
<div ng-show="silences.length == 0">No silences configured</div>
<div ng-hide="silences.length == 0" id="silences-list">

View File

@ -32,6 +32,5 @@
</div>
</div>
<script src="lib/d3.v3.min.js"></script>
<script src="lib/js-yaml.min.js"></script>
<script src="lib/routing-tree.js"></script>
</div>

File diff suppressed because one or more lines are too long

6
ui/lib/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -43,7 +43,7 @@ function resetSVG() {
// Handler for reading config.yml
d3.json("api/v1/status", function(error, data) {
var parsedConfig = jsyaml.load(data.data.config);
var parsedConfig = data.data.configJSON;
// Create a new SVG for each time a config is loaded.
resetSVG();

File diff suppressed because one or more lines are too long