ui: backport UI improvements
This commit is contained in:
parent
8dd8ef08f7
commit
21f0299bc5
File diff suppressed because one or more lines are too long
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
139
ui/app/js/app.js
139
ui/app/js/app.js
|
@ -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,21 +266,70 @@ angular.module('am.controllers').controller('SilenceCtrl',
|
|||
}
|
||||
);
|
||||
|
||||
angular.module('am.controllers').controller('SilencesCtrl',
|
||||
function($scope, Silence) {
|
||||
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.order = "endsAt";
|
||||
|
||||
$scope.showForm = false;
|
||||
|
||||
$scope.toggleForm = function() {
|
||||
$scope.showForm = !$scope.showForm
|
||||
$scope.showForm = !$scope.showForm;
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$scope.pageChanged = function() {
|
||||
$location.search('page', $scope.currentPage);
|
||||
};
|
||||
|
||||
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() {
|
||||
Silence.query({},
|
||||
function(data) {
|
||||
$scope.silences = data.data || [];
|
||||
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']);
|
||||
}
|
||||
|
||||
Silence.query(params, function(resp) {
|
||||
var data = resp.data;
|
||||
$scope.silences = data.silences || [];
|
||||
$scope.totalItems = data.totalSilences;
|
||||
var now = new Date;
|
||||
|
||||
angular.forEach($scope.silences, function(value) {
|
||||
|
@ -289,26 +341,36 @@ angular.module('am.controllers').controller('SilencesCtrl',
|
|||
value.pending = value.startsAt > now;
|
||||
value.active = value.startsAt <= now && value.endsAt > now;
|
||||
});
|
||||
},
|
||||
function(data) {
|
||||
}, 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.refresh();
|
||||
$scope.elapsed = function(elapsed) {
|
||||
return function(sil) {
|
||||
if (elapsed) {
|
||||
return sil.endsAt <= new Date;
|
||||
}
|
||||
);
|
||||
return sil.endsAt > new Date;
|
||||
}
|
||||
};
|
||||
|
||||
angular.module('am.controllers').controller('SilenceCreateCtrl',
|
||||
function($scope, Silence) {
|
||||
$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 || {};
|
||||
|
||||
|
@ -341,6 +403,11 @@ angular.module('am.controllers').controller('SilenceCreateCtrl',
|
|||
|
||||
$scope.reset();
|
||||
|
||||
$scope.$on('silence-created', function(evt) {
|
||||
$scope.form.$setUntouched();
|
||||
$scope.reset();
|
||||
});
|
||||
|
||||
$scope.addMatcher = function() {
|
||||
$scope.silence.matchers.push({});
|
||||
};
|
||||
|
@ -350,22 +417,12 @@ angular.module('am.controllers').controller('SilenceCreateCtrl',
|
|||
};
|
||||
|
||||
$scope.create = function() {
|
||||
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) {
|
||||
if (origSilence.id) {
|
||||
$scope.silence.id = undefined;
|
||||
}
|
||||
|
||||
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.create($scope.silence, function(data) {
|
||||
if (origSilence.id) {
|
||||
Silence.delete({id: origSilence.id},
|
||||
function(data) {
|
||||
// Only trigger reload after after old silence was deleted.
|
||||
|
@ -378,14 +435,11 @@ angular.module('am.controllers').controller('SilenceCreateCtrl',
|
|||
} else {
|
||||
$scope.$emit('silence-created');
|
||||
}
|
||||
},
|
||||
function(data) {
|
||||
}, function(data) {
|
||||
$scope.error = data.data.error;
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
angular.module('am.services').factory('Status',
|
||||
function($resource) {
|
||||
|
@ -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', {
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</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">
|
||||
|
|
|
@ -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>
|
||||
<div class="row" class="silence-matchers">
|
||||
<div class="col-sm-3">
|
||||
<label>Start</label>
|
||||
<input ng-model="silence.startsAt" type="datetime-local" name="start-time" required>
|
||||
</column>
|
||||
<column>
|
||||
<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>
|
||||
<input ng-model="silence.endsAt" type="datetime-local" name="end-time" required>
|
||||
</column>
|
||||
</row>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
</column>
|
||||
</row>
|
||||
|
||||
<row>
|
||||
<column cols="2">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-3">
|
||||
<label>Creator</label>
|
||||
<input ng-model="silence.createdBy" type="email" name="creator" placeholder="me@company.com" required>
|
||||
</column>
|
||||
<column cols="4">
|
||||
<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>
|
||||
<input ng-model="silence.comment" type="text" name="comment" placeholder="reason for silence..." required>
|
||||
</column>
|
||||
</row>
|
||||
<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>
|
||||
|
||||
<div ng-show="error != null" class="alert alert-error">
|
||||
<span class="error">{{ error }}</span>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
@ -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' : '' }}">
|
||||
<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 }}"
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</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 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>
|
||||
|
||||
<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">
|
||||
|
|
|
@ -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 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 ng-show="showForm">
|
||||
<silence-form sil="silence"></silence-form>
|
||||
<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">
|
||||
|
|
|
@ -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>
|
||||
|
|
137
ui/bindata.go
137
ui/bindata.go
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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
Loading…
Reference in New Issue