If enabled, allow users and orgs to set their time machine expiration

Fixes https://www.pivotaltracker.com/story/show/142881203
This commit is contained in:
Joseph Schorr 2017-04-05 14:01:55 -04:00
parent eb5cebbcdf
commit 3dcbe3c631
25 changed files with 472 additions and 15 deletions

View file

@ -21,6 +21,8 @@ angular.module("core-config-setup", ['angularFileUpload'])
{'id': 'registry-storage', 'title': 'Registry Storage'},
{'id': 'time-machine', 'title': 'Time Machine'},
{'id': 'ssl', 'title': 'SSL certificate and key', 'condition': function(config) {
return config.PREFERRED_URL_SCHEME == 'https';
}},
@ -826,7 +828,8 @@ angular.module("core-config-setup", ['angularFileUpload'])
'binding': '=binding',
'placeholder': '@placeholder',
'defaultValue': '@defaultValue',
'itemTitle': '@itemTitle'
'itemTitle': '@itemTitle',
'itemPattern': '@itemPattern'
},
controller: function($scope, $element) {
$scope.removeItem = function(item) {
@ -853,6 +856,20 @@ angular.module("core-config-setup", ['angularFileUpload'])
$scope.newItemName = null;
};
$scope.patternMap = {};
$scope.getRegexp = function(pattern) {
if (!pattern) {
pattern = '.*';
}
if ($scope.patternMap[pattern]) {
return $scope.patternMap[pattern];
}
return $scope.patternMap[pattern] = new RegExp(pattern);
};
$scope.$watch('binding', function(binding) {
if (!binding && $scope.defaultValue) {
$scope.binding = eval($scope.defaultValue);

View file

@ -0,0 +1,104 @@
// From: https://github.com/angular/angular.js/issues/6726#issuecomment-116251130
angular.module('quay').directive('rangeSlider', [function () {
return {
replace: true,
restrict: 'E',
require: 'ngModel',
template: '<input type="range"/>',
link: function (scope, element, attrs, ngModel) {
var ngRangeMin;
var ngRangeMax;
var ngRangeStep;
var value;
function init() {
if (!angular.isDefined(attrs.ngRangeMin)) {
ngRangeMin = 0;
} else {
scope.$watch(attrs.ngRangeMin, function (newValue, oldValue, scope) {
if (angular.isDefined(newValue)) {
ngRangeMin = newValue;
setValue();
}
});
}
if (!angular.isDefined(attrs.ngRangeMax)) {
ngRangeMax = 100;
} else {
scope.$watch(attrs.ngRangeMax, function (newValue, oldValue, scope) {
if (angular.isDefined(newValue)) {
ngRangeMax = newValue;
setValue();
}
});
}
if (!angular.isDefined(attrs.ngRangeStep)) {
ngRangeStep = 1;
} else {
scope.$watch(attrs.ngRangeStep, function (newValue, oldValue, scope) {
if (angular.isDefined(newValue)) {
ngRangeStep = newValue;
setValue();
}
});
}
if (!angular.isDefined(ngModel)) {
value = 50;
} else {
scope.$watch(
function () {
return ngModel.$modelValue;
},
function (newValue, oldValue, scope) {
if (angular.isDefined(newValue)) {
value = newValue;
setValue();
}
}
);
}
if (!ngModel) {
return;
}
ngModel.$parsers.push(function (value) {
var val = Number(value);
if (val !== val) {
val = undefined;
}
return val;
});
}
function setValue() {
if (
angular.isDefined(ngRangeMin) &&
angular.isDefined(ngRangeMax) &&
angular.isDefined(ngRangeStep) &&
angular.isDefined(value)
) {
element.attr("min", ngRangeMin);
element.attr("max", ngRangeMax);
element.attr("step", ngRangeStep);
element.val(value);
}
}
function read() {
if (angular.isDefined(ngModel)) {
ngModel.$setViewValue(value);
}
}
element.on('change', function () {
if (angular.isDefined(value) && (value != element.val())) {
value = element.val();
scope.$apply(read);
}
});
init();
}
};
}
]);

View file

@ -0,0 +1,4 @@
<div class="duration-input-element">
<range-slider ng-range-min="$ctrl.min_s" ng-range-max="$ctrl.max_s" ng-model="$ctrl.seconds"></range-slider>
<span class="duration-explanation">{{ $ctrl.durationExplanation($ctrl.seconds) }}</span>
</div>

View file

@ -0,0 +1,57 @@
import { Input, Output, Component, Inject } from 'ng-metadata/core';
import * as moment from "moment";
/**
* A component that allows for selecting a time duration.
*/
@Component({
selector: 'duration-input',
templateUrl: '/static/js/directives/ui/duration-input/duration-input.component.html'
})
export class DurationInputComponent implements ng.IComponentController {
@Input('<') public min: string;
@Input('<') public max: string;
@Input('=?') public value: string;
@Input('=?') public seconds: number;
private min_s: number;
private max_s: number;
constructor (@Inject('$scope') private $scope: ng.IScope) {
}
public $onInit(): void {
// TODO: replace this.
this.$scope.$watch(() => this.seconds, this.updateValue.bind(this));
this.refresh();
}
public $onChanges(changes: ng.IOnChangesObject): void {
this.refresh();
}
private updateValue(): void {
this.value = this.seconds + 's';
}
private refresh(): void {
this.min_s = this.toSeconds(this.min || '0s');
this.max_s = this.toSeconds(this.max || '1h');
if (this.value) {
this.seconds = this.toSeconds(this.value || '0s')
};
}
private durationExplanation(durationSeconds: string): string {
return moment.duration(parseInt(durationSeconds), 's').humanize();
}
private toSeconds(durationStr: string): number {
var number = durationStr.substring(0, durationStr.length - 1);
var suffix = durationStr.substring(durationStr.length - 1);
return moment.duration(parseInt(number), <moment.unitOfTime.Base>suffix).asSeconds();
}
}

View file

@ -0,0 +1,22 @@
<div class="time-machine-settings-element" ng-if="$ctrl.maximum != '0s' && $ctrl.Features.CHANGE_TAG_EXPIRATION">
<table class="co-list-table">
<tr>
<td>Time Machine:</td>
<td ng-show="$ctrl.updating">
<div class="cor-loader-inline"></div>
</td>
<td ng-show="!$ctrl.updating" ng-if="$ctrl.Config.TAG_EXPIRATION_OPTIONS.length">
<select class="form-control" ng-model="$ctrl.current_s"
ng-options="$ctrl.getSeconds(o) as $ctrl.durationExplanation($ctrl.getSeconds(o)) for o in $ctrl.Config.TAG_EXPIRATION_OPTIONS">
</select>
<div class="help-text">The amount of time, after a tag is deleted, that the tag is accessible in time machine before being garbage collected.</div>
<a class="btn btn-primary save-expiration" ng-disabled="$ctrl.current_s == $ctrl.initial_s" ng-click="$ctrl.updateExpiration()">Save Expiration Time</a>
</td>
<td ng-show="!$ctrl.updating" ng-if="!$ctrl.Config.TAG_EXPIRATION_OPTIONS.length">
<div>{{ $ctrl.durationExplanation($ctrl.current_s) }}</div>
<div class="help-text">The amount of time, after a tag is deleted, that the tag is accessible in time machine before being garbage collected.</div>
</td>
</tr>
</table>
</div>

View file

@ -0,0 +1,72 @@
import { Input, Component, Inject } from 'ng-metadata/core';
import * as moment from "moment";
/**
* A component that displays settings for a namespace for time machine.
*/
@Component({
selector: 'timeMachineSettings',
templateUrl: '/static/js/directives/ui/time-machine-settings/time-machine-settings.component.html'
})
export class TimeMachineSettingsComponent implements ng.IComponentController {
@Input('<') public user: any;
@Input('<') public organization: any;
private initial_s: number;
private current_s: number;
private updating: boolean;
constructor (@Inject('Config') private Config: any, @Inject('ApiService') private ApiService: any,
@Inject('Features') private Features: any) {
this.current_s = 0;
this.initial_s = 0;
this.updating = false;
}
public $onInit(): void {
if (this.user) {
this.current_s = this.user.tag_expiration_s;
this.initial_s = this.user.tag_expiration_s;
} else if (this.organization) {
this.current_s = this.organization.tag_expiration_s;
this.initial_s = this.organization.tag_expiration_s;
}
}
private getSeconds(durationStr: string): number {
if (!durationStr) {
return 0;
}
var number = durationStr.substring(0, durationStr.length - 1);
var suffix = durationStr.substring(durationStr.length - 1);
return moment.duration(parseInt(number), <moment.unitOfTime.Base>suffix).asSeconds();
}
private durationExplanation(durationSeconds: number): string {
return moment.duration(durationSeconds || 0, 's').humanize();
}
private updateExpiration(): void {
this.updating = true;
var errorDisplay = this.ApiService.errorDisplay('Could not update time machine setting', () => {
this.updating = false;
})
var method = (this.user ? this.ApiService.changeUserDetails :
this.ApiService.changeOrganizationDetails);
var params = {};
if (this.organization) {
params['orgname'] = this.organization.name;
}
var data = {
'tag_expiration_s': this.current_s,
};
method(data, params).then((resp) => {
this.updating = false;
this.initial_s = this.current_s;
}, errorDisplay);
}
}

View file

@ -16,6 +16,8 @@ import { CorTableColumn } from './directives/ui/cor-table/cor-table-col.componen
import { ChannelIconComponent } from './directives/ui/channel-icon/channel-icon.component';
import { TagSigningDisplayComponent } from './directives/ui/tag-signing-display/tag-signing-display.component';
import { RepositorySigningConfigComponent } from './directives/ui/repository-signing-config/repository-signing-config.component';
import { TimeMachineSettingsComponent } from './directives/ui/time-machine-settings/time-machine-settings.component';
import { DurationInputComponent } from './directives/ui/duration-input/duration-input.component';
import { BuildServiceImpl } from './services/build/build.service.impl';
import { AvatarServiceImpl } from './services/avatar/avatar.service.impl';
import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service.impl';
@ -48,6 +50,8 @@ import { QuayRequireDirective } from './directives/structural/quay-require/quay-
QuayRequireDirective,
TagSigningDisplayComponent,
RepositorySigningConfigComponent,
TimeMachineSettingsComponent,
DurationInputComponent,
],
providers: [
ViewArrayImpl,