initial import for Open Source 🎉
This commit is contained in:
parent
1898c361f3
commit
9c0dd3b722
2048 changed files with 218743 additions and 0 deletions
|
@ -0,0 +1,61 @@
|
|||
<div ng-if="$ctrl.state === 'choice' && $ctrl.kubeNamespace === false">
|
||||
<div class="co-dialog modal fade initial-setup-modal in" id="setupModal" style="display: block;">
|
||||
<div class="modal-backdrop fade in" style="height: 1000px;"></div>
|
||||
<div class="modal-dialog fade in">
|
||||
<div class="modal-content">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title"><span>Choose an option</span></h4>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div class="config-setup-wrapper">
|
||||
<a class="config-setup_option" ng-click="$ctrl.chooseSetup()">
|
||||
<i class="fas fa-edit fa-2x"></i>
|
||||
<div>Start New Registry Setup</div>
|
||||
</a>
|
||||
<a class="config-setup_option" ng-click="$ctrl.chooseLoad()">
|
||||
<i class="fas fa-upload fa-2x"></i>
|
||||
<div>Modify an existing configuration</div>
|
||||
</a>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'choice' && $ctrl.kubeNamespace !== false">
|
||||
<div class="co-dialog modal fade initial-setup-modal in" id="kubeSetupModal" style="display: block;">
|
||||
<div class="modal-backdrop fade in" style="height: 1000px;"></div>
|
||||
<div class="modal-dialog fade in">
|
||||
<div class="modal-content">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title"><span>Choose an option for the <code>{{ $ctrl.kubeNamespace }}</code> namespace</span></h4>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div class="config-setup-wrapper">
|
||||
<a class="config-setup_option" ng-click="$ctrl.chooseSetup()">
|
||||
<i class="fas fa-edit fa-2x"></i>
|
||||
<div>Start new configuration for this cluster</div>
|
||||
</a>
|
||||
<a class="config-setup_option" ng-click="$ctrl.choosePopulate()">
|
||||
<i class="fas fa-cloud-download-alt fa-2x"></i>
|
||||
<div>Modify configuration for this cluster</div>
|
||||
</a>
|
||||
<a class="config-setup_option" ng-click="$ctrl.chooseLoad()">
|
||||
<i class="fas fa-upload fa-2x"></i>
|
||||
<div>Populate this cluster from a previously created configuration</div>
|
||||
</a>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'setup'" class="setup" setup-completed="$ctrl.setupCompleted()"></div>
|
||||
<load-config ng-if="$ctrl.state === 'load'" config-loaded="$ctrl.configLoaded()"></load-config>
|
||||
<download-tarball-modal
|
||||
ng-if="$ctrl.state === 'download'"
|
||||
loaded-config="$ctrl.loadedConfig"
|
||||
is-kubernetes="$ctrl.kubeNamespace !== false"
|
||||
choose-deploy="$ctrl.chooseDeploy()">
|
||||
</download-tarball-modal>
|
||||
<kube-deploy-modal ng-if="$ctrl.state === 'deploy'" loaded-config="$ctrl.loadedConfig"></kube-deploy-modal>
|
|
@ -0,0 +1,70 @@
|
|||
import { Component, Inject } from 'ng-metadata/core';
|
||||
const templateUrl = require('./config-setup-app.component.html');
|
||||
|
||||
declare var window: any;
|
||||
|
||||
/**
|
||||
* Initial Screen and Choice in the Config App
|
||||
*/
|
||||
@Component({
|
||||
selector: 'config-setup-app',
|
||||
templateUrl: templateUrl,
|
||||
})
|
||||
export class ConfigSetupAppComponent {
|
||||
private state
|
||||
: 'choice'
|
||||
| 'setup'
|
||||
| 'load'
|
||||
| 'download'
|
||||
| 'deploy';
|
||||
|
||||
private loadedConfig = false;
|
||||
private kubeNamespace: string | boolean = false;
|
||||
|
||||
constructor(@Inject('ApiService') private apiService) {
|
||||
this.state = 'choice';
|
||||
if (window.__kubernetes_namespace) {
|
||||
this.kubeNamespace = window.__kubernetes_namespace;
|
||||
}
|
||||
}
|
||||
|
||||
private chooseSetup(): void {
|
||||
this.apiService.scStartNewConfig()
|
||||
.then(() => {
|
||||
this.state = 'setup';
|
||||
})
|
||||
.catch(this.apiService.errorDisplay(
|
||||
'Could not initialize new setup. Please report this error'
|
||||
));
|
||||
}
|
||||
|
||||
private chooseLoad(): void {
|
||||
this.state = 'load';
|
||||
this.loadedConfig = true;
|
||||
}
|
||||
|
||||
private choosePopulate(): void {
|
||||
this.apiService.scKubePopulateConfig()
|
||||
.then(() => {
|
||||
this.state = 'setup';
|
||||
this.loadedConfig = true;
|
||||
})
|
||||
.catch(err => {
|
||||
this.apiService.errorDisplay(
|
||||
`Could not populate the configuration from your cluster. Please report this error: ${JSON.stringify(err)}`
|
||||
)()
|
||||
})
|
||||
}
|
||||
|
||||
private configLoaded(): void {
|
||||
this.state = 'setup';
|
||||
}
|
||||
|
||||
private setupCompleted(): void {
|
||||
this.state = 'download';
|
||||
}
|
||||
|
||||
private chooseDeploy(): void {
|
||||
this.state = 'deploy';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<div class="co-floating-bottom-bar">
|
||||
<span ng-transclude/>
|
||||
</div>
|
|
@ -0,0 +1,44 @@
|
|||
const templateUrl = require('./cor-floating-bottom-bar.html');
|
||||
|
||||
angular.module('quay-config')
|
||||
.directive('corFloatingBottomBar', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 3,
|
||||
templateUrl,
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element, $timeout, $interval) {
|
||||
var handler = function() {
|
||||
$element.removeClass('floating');
|
||||
$element.css('width', $element[0].parentNode.clientWidth + 'px');
|
||||
|
||||
var windowHeight = $(window).height();
|
||||
var rect = $element[0].getBoundingClientRect();
|
||||
if (rect.bottom > windowHeight) {
|
||||
$element.addClass('floating');
|
||||
}
|
||||
};
|
||||
|
||||
$(window).on("scroll", handler);
|
||||
$(window).on("resize", handler);
|
||||
|
||||
var previousHeight = $element[0].parentNode.clientHeight;
|
||||
var stop = $interval(function() {
|
||||
var currentHeight = $element[0].parentNode.clientWidth;
|
||||
if (previousHeight != currentHeight) {
|
||||
currentHeight = previousHeight;
|
||||
handler();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
$(window).off("resize", handler);
|
||||
$(window).off("scroll", handler);
|
||||
$interval.cancel(stop);
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
<div class="co-m-inline-loader co-an-fade-in-out">
|
||||
<div class="co-m-loader-dot__one"></div>
|
||||
<div class="co-m-loader-dot__two"></div>
|
||||
<div class="co-m-loader-dot__three"></div>
|
||||
</div>
|
5
config_app/js/components/cor-loader/cor-loader.html
Normal file
5
config_app/js/components/cor-loader/cor-loader.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<div class="co-m-loader co-an-fade-in-out">
|
||||
<div class="co-m-loader-dot__one"></div>
|
||||
<div class="co-m-loader-dot__two"></div>
|
||||
<div class="co-m-loader-dot__three"></div>
|
||||
</div>
|
28
config_app/js/components/cor-loader/cor-loader.js
Normal file
28
config_app/js/components/cor-loader/cor-loader.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
const loaderUrl = require('./cor-loader.html');
|
||||
const inlineUrl = require('./cor-loader-inline.html');
|
||||
|
||||
angular.module('quay-config')
|
||||
.directive('corLoader', function() {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: loaderUrl,
|
||||
replace: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
.directive('corLoaderInline', function() {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: inlineUrl,
|
||||
replace: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
3
config_app/js/components/cor-option/cor-option.html
Normal file
3
config_app/js/components/cor-option/cor-option.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<li>
|
||||
<a ng-click="optionClick()" ng-transclude></a>
|
||||
</li>
|
32
config_app/js/components/cor-option/cor-option.js
Normal file
32
config_app/js/components/cor-option/cor-option.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
const corOption = require('./cor-option.html');
|
||||
const corOptionsMenu = require('./cor-options-menu.html');
|
||||
|
||||
angular.module('quay-config')
|
||||
.directive('corOptionsMenu', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: corOptionsMenu,
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
.directive('corOption', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: corOption,
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'optionClick': '&optionClick'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
<span class="co-options-menu">
|
||||
<div class="dropdown" style="text-align: left;">
|
||||
<i class="fas fa-cog fa-lg dropdown-toggle" data-toggle="dropdown" data-title="Options" bs-tooltip></i>
|
||||
<ul class="dropdown-menu pull-right" ng-transclude></ul>
|
||||
</div>
|
||||
</span>
|
|
@ -0,0 +1,4 @@
|
|||
<div class="cor-progress-bar-element progress">
|
||||
<div class="progress-bar" ng-style="{'width': (progress * 100) + '%'}"
|
||||
aria-valuenow="{{ (progress * 100) }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
|
@ -0,0 +1,74 @@
|
|||
|
||||
|
||||
const corStepBarUrl = require('./cor-step-bar.html');
|
||||
const corStepUrl = require('./cor-step.html');
|
||||
const corProgressBarUrl = require('./cor-progress-bar.html');
|
||||
|
||||
angular.module('quay-config')
|
||||
.directive('corStepBar', () => {
|
||||
const directiveDefinitionObject = {
|
||||
priority: 4,
|
||||
templateUrl: corStepBarUrl,
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'progress': '=?progress'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
$scope.$watch('progress', function(progress) {
|
||||
if (!progress) { return; }
|
||||
|
||||
var index = 0;
|
||||
for (var i = 0; i < progress.length; ++i) {
|
||||
if (progress[i]) {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
|
||||
$element.find('.transclude').children('.co-step-element').each(function(i, elem) {
|
||||
$(elem).removeClass('active');
|
||||
if (i <= index) {
|
||||
$(elem).addClass('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corStep', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 4,
|
||||
templateUrl: corStepUrl,
|
||||
replace: true,
|
||||
transclude: false,
|
||||
requires: '^corStepBar',
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'icon': '@icon',
|
||||
'title': '@title',
|
||||
'text': '@text'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
.directive('corProgressBar', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 4,
|
||||
templateUrl: corProgressBarUrl,
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'progress': '=progress'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
3
config_app/js/components/cor-progress/cor-step-bar.html
Normal file
3
config_app/js/components/cor-progress/cor-step-bar.html
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="co-step-bar">
|
||||
<span class="transclude" ng-transclude/>
|
||||
</div>
|
6
config_app/js/components/cor-progress/cor-step.html
Normal file
6
config_app/js/components/cor-progress/cor-step.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
<span ng-class="text ? 'co-step-element text' : 'co-step-element icon'">
|
||||
<span data-title="{{ title }}" bs-tooltip>
|
||||
<span class="text" ng-if="text">{{ text }}</span>
|
||||
<i class="fa fa-lg" ng-if="icon" ng-class="'fa-' + icon"></i>
|
||||
</span>
|
||||
</span>
|
|
@ -0,0 +1,3 @@
|
|||
<div class="col-lg-6 col-md-6 col-sm-5 col-xs-12">
|
||||
<h2 class="co-nav-title-content co-fx-text-shadow" ng-transclude></h2>
|
||||
</div>
|
2
config_app/js/components/cor-title/cor-title.html
Normal file
2
config_app/js/components/cor-title/cor-title.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
<div class="co-nav-title" ng-transclude></div>
|
||||
|
31
config_app/js/components/cor-title/cor-title.js
Normal file
31
config_app/js/components/cor-title/cor-title.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
|
||||
const titleUrl = require('./cor-title.html');
|
||||
const titleContentUrl = require('./cor-title-content.html');
|
||||
|
||||
angular.module('quay-config')
|
||||
.directive('corTitleContent', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: titleContentUrl,
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
.directive('corTitle', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl: titleUrl,
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
<span class="datetime-picker-element">
|
||||
<input class="form-control" type="text" ng-model="selected_datetime"/>
|
||||
</span>
|
60
config_app/js/components/datetime-picker/datetime-picker.js
Normal file
60
config_app/js/components/datetime-picker/datetime-picker.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
const templateUrl = require('./datetime-picker.html');
|
||||
/**
|
||||
* An element which displays a datetime picker.
|
||||
*/
|
||||
angular.module('quay-config').directive('datetimePicker', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl,
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'datetime': '=datetime',
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
var datetimeSet = false;
|
||||
|
||||
$(function() {
|
||||
$element.find('input').datetimepicker({
|
||||
'format': 'LLL',
|
||||
'sideBySide': true,
|
||||
'showClear': true,
|
||||
'minDate': new Date(),
|
||||
'debug': false
|
||||
});
|
||||
|
||||
$element.find('input').on("dp.change", function (e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.datetime = e.date ? e.date.unix() : null;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('selected_datetime', function(value) {
|
||||
if (!datetimeSet) { return; }
|
||||
|
||||
if (!value) {
|
||||
if ($scope.datetime) {
|
||||
$scope.datetime = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.datetime = (new Date(value)).getTime()/1000;
|
||||
});
|
||||
|
||||
$scope.$watch('datetime', function(value) {
|
||||
if (!value) {
|
||||
$scope.selected_datetime = null;
|
||||
datetimeSet = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.selected_datetime = moment.unix(value).format('LLL');
|
||||
datetimeSet = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
<div>
|
||||
<div class="co-dialog modal fade initial-setup-modal in" id="setupModal" style="display: block;">
|
||||
<div class="modal-backdrop fade in" style="height: 1000px;"></div>
|
||||
<div class="modal-dialog fade in">
|
||||
<div class="modal-content">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<span class="cor-step-bar">
|
||||
<span class="cor-step active" title="Configure Database" text="1"></span>
|
||||
<span class="cor-step active" title="Setup Database" icon="database"></span>
|
||||
<span class="cor-step active" title="Create Superuser" text="2"></span>
|
||||
<span class="cor-step active" title="Configure Registry" text="3"></span>
|
||||
<span class="cor-step active" title="Validate Configuration" text="4"></span>
|
||||
<span class="cor-step active" title="Setup Complete" icon="download"></span>
|
||||
</span>
|
||||
<h4 class="modal-title"><span>Download Configuration</span></h4>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div class="modal-body download-tarball-modal">
|
||||
<div ng-if="$ctrl.loadedConfig">
|
||||
Please download your updated configuration. To deploy these changes to your Red Hat Quay instances, please
|
||||
<a target="_blank" href="https://coreos.com/quay-enterprise/docs/latest/initial-setup.html">
|
||||
see the docs.
|
||||
</a>
|
||||
<div class="modal__warning-box">
|
||||
<div class="co-alert co-alert-warning">
|
||||
<strong>Warning: </strong>
|
||||
Your configuration and certificates are kept <i>unencrypted</i>. Please keep this file secure.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="!$ctrl.loadedConfig">
|
||||
Please download your new configuration. For more information, and next steps, please
|
||||
<a target="_blank" href="https://coreos.com/quay-enterprise/docs/latest/initial-setup.html">
|
||||
see the docs.
|
||||
</a>
|
||||
<div class="modal__warning-box">
|
||||
<div class="co-alert co-alert-warning">
|
||||
<strong>Warning: </strong>
|
||||
Your configuration and certificates are kept <i>unencrypted</i>. Please keep this file secure.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-default btn-lg download-button"
|
||||
ng-click="$ctrl.downloadTarball()">
|
||||
<i class="fa fa-download" style="margin-right: 10px;"></i>Download Configuration
|
||||
</button>
|
||||
</div>
|
||||
<div ng-if="$ctrl.isKubernetes" class="modal-footer">
|
||||
<button class="btn btn-primary"
|
||||
ng-click="$ctrl.goToDeploy()">
|
||||
<i class="far fa-paper-plane" style="margin-right: 10px;"></i>Go to deployment rollout
|
||||
</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,40 @@
|
|||
import {Input, Component, Inject, Output, EventEmitter} from 'ng-metadata/core';
|
||||
const templateUrl = require('./download-tarball-modal.component.html');
|
||||
const styleUrl = require('./download-tarball-modal.css');
|
||||
|
||||
declare const FileSaver: any;
|
||||
|
||||
/**
|
||||
* Initial Screen and Choice in the Config App
|
||||
*/
|
||||
@Component({
|
||||
selector: 'download-tarball-modal',
|
||||
templateUrl: templateUrl,
|
||||
styleUrls: [ styleUrl ],
|
||||
})
|
||||
export class DownloadTarballModalComponent {
|
||||
@Input('<') public loadedConfig;
|
||||
@Input('<') public isKubernetes;
|
||||
@Output() public chooseDeploy = new EventEmitter<any>();
|
||||
|
||||
constructor(@Inject('ApiService') private ApiService) {
|
||||
|
||||
}
|
||||
|
||||
private downloadTarball(): void {
|
||||
const errorDisplay: Function = this.ApiService.errorDisplay(
|
||||
'Could not save configuration. Please report this error.'
|
||||
);
|
||||
|
||||
// We need to set the response type to 'blob', to ensure it's never encoded as a string
|
||||
// (string encoded binary data can be difficult to transform with js)
|
||||
// and to make it easier to save (FileSaver expects a blob)
|
||||
this.ApiService.scGetConfigTarball(null, null, null, null, 'blob').then(function(resp) {
|
||||
FileSaver.saveAs(resp, 'quay-config.tar.gz');
|
||||
}, errorDisplay);
|
||||
}
|
||||
|
||||
private goToDeploy() {
|
||||
this.chooseDeploy.emit({});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
.co-dialog .modal-body.download-tarball-modal {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal__warning-box {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.download-tarball-modal .download-button {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.download-tarball-modal .download-button:hover {
|
||||
background-color: #dddddd;
|
||||
text-decoration: none;
|
||||
}
|
46
config_app/js/components/file-upload-box.html
Normal file
46
config_app/js/components/file-upload-box.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<div class="file-upload-box-element">
|
||||
<div class="file-input-container">
|
||||
<div ng-show="state != 'uploading'">
|
||||
<form id="file-drop-form-{{ boxId }}">
|
||||
<input id="file-drop-{{ boxId }}" name="file-drop-{{ boxId }}" class="file-drop"
|
||||
type="file" files-changed="handleFilesChanged(files)"
|
||||
accept="{{ getAccepts(extensions) }}">
|
||||
<label for="file-drop-{{ boxId }}" ng-class="state">
|
||||
<span class="chosen-file">
|
||||
<span ng-if="selectedFiles.length">
|
||||
{{ selectedFiles[0].name }}
|
||||
<span ng-if="selectedFiles.length > 1">
|
||||
and {{ selectedFiles.length - 1 }} others...
|
||||
</span>
|
||||
</span>
|
||||
</span><span class="choose-button">
|
||||
<span>Select file</span>
|
||||
</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="cor-loader-line" ng-if="state == 'checking'"></div>
|
||||
|
||||
<div class="status-message" ng-if="state == 'uploading'">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
aria-valuenow="{{ uploadProgress }}" aria-valuemin="0" aria-valuemax="100"
|
||||
style="{{ 'width: ' + uploadProgress + '%' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Uploading file {{ currentlyUploadingFile.name }}...
|
||||
</div>
|
||||
|
||||
<div class="select-message" ng-if="state == 'clear'">{{ selectMessage }}</div>
|
||||
<div class="status-message error-message" ng-if="state == 'error'">
|
||||
<i class="fa fa-times-circle"></i>
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="status-message okay-message" ng-if="state == 'okay'">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
169
config_app/js/components/file-upload-box.js
Normal file
169
config_app/js/components/file-upload-box.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
const templateUrl = require('./file-upload-box.html');
|
||||
/**
|
||||
* An element which adds a stylize box for uploading a file.
|
||||
*/
|
||||
angular.module('quay-config').directive('fileUploadBox', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl,
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'selectMessage': '@selectMessage',
|
||||
|
||||
'filesSelected': '&filesSelected',
|
||||
'filesCleared': '&filesCleared',
|
||||
'filesValidated': '&filesValidated',
|
||||
|
||||
'extensions': '<extensions',
|
||||
'apiEndpoint': '@apiEndpoint',
|
||||
|
||||
'reset': '=?reset'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, ApiService) {
|
||||
var MEGABYTE = 1000000;
|
||||
var MAX_FILE_SIZE = MAX_FILE_SIZE_MB * MEGABYTE;
|
||||
var MAX_FILE_SIZE_MB = 100;
|
||||
|
||||
var number = $rootScope.__fileUploadBoxIdCounter || 0;
|
||||
$rootScope.__fileUploadBoxIdCounter = number + 1;
|
||||
|
||||
$scope.boxId = number;
|
||||
$scope.state = 'clear';
|
||||
$scope.selectedFiles = [];
|
||||
|
||||
var conductUpload = function(file, apiEndpoint, fileId, mimeType, progressCb, doneCb) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('PUT', '/api/v1/' + apiEndpoint, true);
|
||||
request.setRequestHeader('Content-Type', mimeType);
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
if (e.lengthComputable) { progressCb((e.loaded / e.total) * 100); }
|
||||
});
|
||||
};
|
||||
|
||||
request.onerror = function() {
|
||||
$scope.$apply(function() { doneCb(false, 'Error when uploading'); });
|
||||
};
|
||||
|
||||
request.onreadystatechange = function() {
|
||||
var state = request.readyState;
|
||||
var status = request.status;
|
||||
|
||||
if (state == 4) {
|
||||
if (Math.floor(status / 100) == 2) {
|
||||
$scope.$apply(function() { doneCb(true, fileId); });
|
||||
} else {
|
||||
var message = request.statusText;
|
||||
if (status == 413) {
|
||||
message = 'Selected file too large to upload';
|
||||
}
|
||||
|
||||
$scope.$apply(function() { doneCb(false, message); });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
request.send(file);
|
||||
};
|
||||
|
||||
var uploadFiles = function(callback) {
|
||||
var currentIndex = 0;
|
||||
var fileIds = [];
|
||||
|
||||
var progressCb = function(progress) {
|
||||
$scope.uploadProgress = progress;
|
||||
};
|
||||
|
||||
var doneCb = function(status, messageOrId) {
|
||||
if (status) {
|
||||
fileIds.push(messageOrId);
|
||||
currentIndex++;
|
||||
performFileUpload();
|
||||
} else {
|
||||
callback(false, messageOrId);
|
||||
}
|
||||
};
|
||||
|
||||
var performFileUpload = function() {
|
||||
// If we have finished uploading all of the files, invoke the overall callback
|
||||
// with the list of file IDs.
|
||||
if (currentIndex >= $scope.selectedFiles.length) {
|
||||
callback(true, fileIds);
|
||||
return;
|
||||
}
|
||||
|
||||
// For the current file, retrieve a file-drop URL from the API for the file.
|
||||
var currentFile = $scope.selectedFiles[currentIndex];
|
||||
var mimeType = currentFile.type || 'application/octet-stream';
|
||||
var data = {
|
||||
'mimeType': mimeType
|
||||
};
|
||||
|
||||
$scope.currentlyUploadingFile = currentFile;
|
||||
$scope.uploadProgress = 0;
|
||||
|
||||
conductUpload(currentFile, $scope.apiEndpoint, $scope.selectedFiles[0].name, mimeType, progressCb, doneCb);
|
||||
};
|
||||
|
||||
// Start the uploading.
|
||||
$scope.state = 'uploading';
|
||||
performFileUpload();
|
||||
};
|
||||
|
||||
$scope.handleFilesChanged = function(files) {
|
||||
if ($scope.state == 'uploading') { return; }
|
||||
|
||||
$scope.message = null;
|
||||
$scope.selectedFiles = files;
|
||||
|
||||
if (files.length == 0) {
|
||||
$scope.state = 'clear';
|
||||
$scope.filesCleared();
|
||||
} else {
|
||||
for (var i = 0; i < files.length; ++i) {
|
||||
if (files[i].size > MAX_FILE_SIZE) {
|
||||
$scope.state = 'error';
|
||||
$scope.message = 'File ' + files[i].name + ' is larger than the maximum file ' +
|
||||
'size of ' + MAX_FILE_SIZE_MB + ' MB';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.state = 'checking';
|
||||
$scope.filesSelected({
|
||||
'files': files,
|
||||
'callback': function(status, message) {
|
||||
$scope.state = status ? 'okay' : 'error';
|
||||
$scope.message = message;
|
||||
|
||||
if (status) {
|
||||
$scope.filesValidated({
|
||||
'files': files,
|
||||
'uploadFiles': uploadFiles
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getAccepts = function(extensions) {
|
||||
if (!extensions || !extensions.length) {
|
||||
return '*';
|
||||
}
|
||||
|
||||
return extensions.join(',');
|
||||
};
|
||||
|
||||
$scope.$watch('reset', function(reset) {
|
||||
if (reset) {
|
||||
$scope.state = 'clear';
|
||||
$element.find('#file-drop-' + $scope.boxId).parent().trigger('reset');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
18
config_app/js/components/files-changed.js
Normal file
18
config_app/js/components/files-changed.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Raises the 'filesChanged' event on the scope if a file on the marked <input type="file"> exists.
|
||||
*/
|
||||
angular.module('quay-config').directive("filesChanged", [function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
'filesChanged': "&"
|
||||
},
|
||||
link: function (scope, element, attributes) {
|
||||
element.bind("change", function (changeEvent) {
|
||||
scope.$apply(function() {
|
||||
scope.filesChanged({'files': changeEvent.target.files});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
|
@ -0,0 +1,71 @@
|
|||
<div>
|
||||
<div class="co-dialog modal fade initial-setup-modal in" id="setupModal" style="display: block;">
|
||||
<div class="modal-backdrop fade in" style="height: 1000px;"></div>
|
||||
<div class="modal-dialog fade in">
|
||||
<div class="modal-content kube-deploy-modal">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<span class="cor-step-bar">
|
||||
<span class="cor-step active" title="Configure Database" text="1"></span>
|
||||
<span class="cor-step active" title="Setup Database" icon="database"></span>
|
||||
<span class="cor-step active" title="Create Superuser" text="2"></span>
|
||||
<span class="cor-step active" title="Configure Registry" text="3"></span>
|
||||
<span class="cor-step active" title="Validate Configuration" text="4"></span>
|
||||
<span class="cor-step active" title="Setup Complete" icon="download"></span>
|
||||
<span class="cor-step active" title="Deploy Complete" icon="paper-plane"></span>
|
||||
</span>
|
||||
<h4 class="modal-title"><span>Deploy configuration</span></h4>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div class="modal-body">
|
||||
<div class="cor-loader" ng-if="$ctrl.state === 'loadingDeployments'"></div>
|
||||
<div class="kube-deploy-modal__body" ng-if="$ctrl.deploymentsStatus.length > 0">
|
||||
<span class="kube-deploy-modal__list-header">The following deployments will be affected:</span>
|
||||
<ul class="kube-deploy-modal__list">
|
||||
<li class="kube-deploy-modal__list-item" ng-repeat="deployment in $ctrl.deploymentsStatus">
|
||||
<i class="fa ci-k8s-logo"></i>
|
||||
<code>{{deployment.name}}</code> with {{deployment.numPods}} <b> {{ deployment.numPods === 1 ? ' pod' : ' pods' }}</b>
|
||||
</li>
|
||||
</ul>
|
||||
<button ng-if="$ctrl.state === 'readyToDeploy'" class="btn btn-primary btn-lg" ng-click="$ctrl.deployConfiguration()">
|
||||
<i class="far fa-paper-plane" style="margin-right: 10px;"></i>
|
||||
Populate configuration to deployments
|
||||
</button>
|
||||
<div ng-if="$ctrl.state === 'deployingConfiguration'">
|
||||
<div class="cor-loader"></div>
|
||||
Deploying configuration...
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'cyclingDeployments'">
|
||||
<div class="cor-loader"></div>
|
||||
<span class="kube-deploy-modal__list-header">Cycling deployments...</span>
|
||||
<ul class="kube-deploy-modal__list">
|
||||
<li class="kube-deploy-modal__list-item" ng-repeat="deployment in $ctrl.deploymentsStatus">
|
||||
<i class="fa ci-k8s-logo"></i>
|
||||
<code>{{deployment.name}}</code>: {{deployment.message || 'Waiting for deployment information...'}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'deployed'" class="modal-footer co-alert co-alert-success">
|
||||
Configuration successfully rolled out and deployed!
|
||||
<br>Note: The web interface of the Quay app may take a few minutes to come up.
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'error'" class="modal-footer alert alert-danger">
|
||||
{{ $ctrl.errorMessage }}
|
||||
<div ng-if="$ctrl.rollingBackStatus !== 'none'" class="rollback">
|
||||
<button ng-if="$ctrl.rollingBackStatus === 'offer'" class="btn btn-default" ng-click="$ctrl.rollbackDeployments()">
|
||||
<i class="fas fa-history" style="margin-right: 10px;"></i>
|
||||
Rollback deployments
|
||||
</button>
|
||||
<div ng-if="$ctrl.rollingBackStatus === 'rolling'" class="cor-loader-inline"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state === 'rolledBackWarning'" class="modal-footer co-alert co-alert-warning">
|
||||
Successfully rolled back changes. Please try deploying again with your configuration later.
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,153 @@
|
|||
import { Input, Component, Inject, OnDestroy } from 'ng-metadata/core';
|
||||
import { AngularPollChannel, PollHandle } from "../../services/services.types";
|
||||
const templateUrl = require('./kube-deploy-modal.component.html');
|
||||
const styleUrl = require('./kube-deploy-modal.css');
|
||||
|
||||
// The response from the API about deployment rollout status
|
||||
type DeploymentRollout = {
|
||||
status: 'available' | 'progressing' | 'failed',
|
||||
message: string
|
||||
};
|
||||
|
||||
type DeploymentStatus = {
|
||||
name: string,
|
||||
numPods: number,
|
||||
message?: string,
|
||||
pollHandler?: PollHandle,
|
||||
}
|
||||
|
||||
const DEPLOYMENT_POLL_SLEEPTIME = 5000; /* 5 seconds */
|
||||
|
||||
@Component({
|
||||
selector: 'kube-deploy-modal',
|
||||
templateUrl,
|
||||
styleUrls: [ styleUrl ],
|
||||
})
|
||||
export class KubeDeployModalComponent implements OnDestroy {
|
||||
@Input('<') public loadedConfig;
|
||||
private state
|
||||
: 'loadingDeployments'
|
||||
| 'readyToDeploy'
|
||||
| 'deployingConfiguration'
|
||||
| 'cyclingDeployments'
|
||||
| 'deployed'
|
||||
| 'error'
|
||||
| 'rolledBackWarning' = 'loadingDeployments';
|
||||
private errorMessage: string;
|
||||
private deploymentsStatus: DeploymentStatus[] = [];
|
||||
private deploymentsCycled: number = 0;
|
||||
private onDestroyListeners: Function[] = [];
|
||||
private rollingBackStatus
|
||||
: 'none'
|
||||
| 'offer'
|
||||
| 'rolling' = 'none';
|
||||
|
||||
constructor(@Inject('ApiService') private ApiService, @Inject('AngularPollChannel') private AngularPollChannel: AngularPollChannel) {
|
||||
ApiService.scGetNumDeployments().then(resp => {
|
||||
this.deploymentsStatus = resp.items.map(dep => ({ name: dep.metadata.name, numPods: dep.spec.replicas }));
|
||||
this.state = 'readyToDeploy';
|
||||
}).catch(err => {
|
||||
this.state = 'error';
|
||||
this.errorMessage = `There are no Quay deployments active in this namespace. \
|
||||
Please check that you are running this \
|
||||
tool in the same namespace as the Red Hat Quay application\
|
||||
Associated error message: ${err.toString()}`;
|
||||
})
|
||||
}
|
||||
|
||||
// Call all listeners of the onDestroy
|
||||
ngOnDestroy(): any {
|
||||
this.onDestroyListeners.forEach(fn => {
|
||||
fn()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
deployConfiguration(): void {
|
||||
this.ApiService.scDeployConfiguration().then(() => {
|
||||
this.state = 'deployingConfiguration';
|
||||
const deploymentNames: string[] = this.deploymentsStatus.map(dep => dep.name);
|
||||
|
||||
this.ApiService.scCycleQEDeployments({ deploymentNames }).then(() => {
|
||||
this.state = 'cyclingDeployments';
|
||||
this.watchDeployments();
|
||||
}).catch(err => {
|
||||
this.state = 'error';
|
||||
this.errorMessage = `Could not cycle the deployments with the new configuration. Error: ${err.toString()}`;
|
||||
})
|
||||
}).catch(err => {
|
||||
this.state = 'error';
|
||||
this.errorMessage = `Could not deploy the configuration. Error: ${err.toString()}`;
|
||||
})
|
||||
}
|
||||
|
||||
watchDeployments(): void {
|
||||
this.deploymentsStatus.forEach(deployment => {
|
||||
const pollChannel = this.AngularPollChannel.create({
|
||||
// Have to mock the scope object for the poll channel since we're calling into angular1 code
|
||||
// We register the onDestroy function to be called later when this object is destroyed
|
||||
'$on': (_, onDestruction) => { this.onDestroyListeners.push(onDestruction) }
|
||||
}, this.getDeploymentStatus(deployment), DEPLOYMENT_POLL_SLEEPTIME);
|
||||
|
||||
pollChannel.start();
|
||||
});
|
||||
}
|
||||
|
||||
// Query each deployment every 5s, and stop polling once it's either available or failed
|
||||
getDeploymentStatus(deployment: DeploymentStatus): (boolean) => void {
|
||||
return (continue_callback: (shouldContinue: boolean) => void) => {
|
||||
const params = {
|
||||
'deployment': deployment.name
|
||||
};
|
||||
|
||||
this.ApiService.scGetDeploymentRolloutStatus(null, params).then((deploymentRollout: DeploymentRollout) => {
|
||||
if (deploymentRollout.status === 'available') {
|
||||
continue_callback(false);
|
||||
|
||||
this.deploymentsCycled++;
|
||||
if (this.deploymentsCycled === this.deploymentsStatus.length) {
|
||||
this.state = 'deployed';
|
||||
}
|
||||
} else if (deploymentRollout.status === 'progressing') {
|
||||
continue_callback(true);
|
||||
deployment.message = deploymentRollout.message;
|
||||
} else { // deployment rollout failed
|
||||
this.state = 'error';
|
||||
continue_callback(false);
|
||||
deployment.message = deploymentRollout.message;
|
||||
this.errorMessage = `Could not cycle deployments: ${deploymentRollout.message}`;
|
||||
|
||||
// Only offer rollback if we loaded/populated a config. (Can't rollback an initial setup)
|
||||
if (this.loadedConfig) {
|
||||
this.rollingBackStatus = 'offer';
|
||||
this.errorMessage = `Could not cycle deployments: ${deploymentRollout.message}`;
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
continue_callback(false);
|
||||
this.state = 'error';
|
||||
this.errorMessage = `Could not cycle the deployments with the new configuration. Error: ${err.toString()}\
|
||||
Would you like to rollback the deployment to its previous state?`;
|
||||
// Only offer rollback if we loaded/populated a config. (Can't rollback an initial setup)
|
||||
if (this.loadedConfig) {
|
||||
this.rollingBackStatus = 'offer';
|
||||
this.errorMessage = `Could not get deployment information for: ${deployment}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rollbackDeployments(): void {
|
||||
this.rollingBackStatus = 'rolling';
|
||||
const deploymentNames: string[] = this.deploymentsStatus.map(dep => dep.name);
|
||||
|
||||
this.ApiService.scRollbackDeployments({ deploymentNames }).then(() => {
|
||||
this.state = 'rolledBackWarning';
|
||||
this.rollingBackStatus = 'none';
|
||||
}).catch(err => {
|
||||
this.rollingBackStatus = 'none';
|
||||
this.state = 'error';
|
||||
this.errorMessage = `Could not cycle the deployments back to their previous states. Please contact support: ${err.toString()}`;
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
.kube-deploy-modal__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.kube-deploy-modal__list {
|
||||
padding-top: 10px;
|
||||
padding-left: 15px;
|
||||
margin-bottom: 15px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.kube-deploy-modal__list-header {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.kube-deploy-modal__list-item {
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.kube-deploy-modal__list-item i {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.kube-deploy-modal__body .btn {
|
||||
align-self: center;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.kube-deploy-modal .rollback {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.kube-deploy-modal .co-alert.co-alert-warning {
|
||||
padding: 25px;
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.kube-deploy-modal .co-alert.co-alert-warning:before {
|
||||
position: static;
|
||||
padding-right: 15px;
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<div>
|
||||
<div class="co-dialog modal fade initial-setup-modal in" id="setupModal" style="display: block;">
|
||||
<div class="modal-backdrop fade in" style="height: 1000px;"></div>
|
||||
<div class="modal-dialog fade in">
|
||||
<div class="modal-content">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<span class="cor-step-bar">
|
||||
<span class="cor-step active" title="Configure Database" text="1"></span>
|
||||
<span class="cor-step" title="Setup Database" icon="database"></span>
|
||||
<span class="cor-step" title="Create Superuser" text="2"></span>
|
||||
<span class="cor-step" title="Configure Registry" text="3"></span>
|
||||
<span class="cor-step" title="Validate Configuration" text="4"></span>
|
||||
<span class="cor-step" title="Setup Complete" icon="download"></span>
|
||||
</span>
|
||||
<h4 class="modal-title"><span>Load Config</span></h4>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div class="modal-body">
|
||||
<span>Please upload the previous configuration</span>
|
||||
<div class="file-upload-box"
|
||||
api-endpoint="configapp/tarconfig"
|
||||
select-message="Select a previous configuration to modify. Must be in tar.gz format"
|
||||
files-selected="$ctrl.handleTarballSelected(files, callback)"
|
||||
files-cleared="$ctrl.handleFilesCleared()"
|
||||
files-validated="$ctrl.tarballValidatedByUploadBox(files, uploadFiles)"
|
||||
extensions="['application/gzip', '.gz']"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="$ctrl.uploadTarball()" ng-disabled="!$ctrl.readyToSubmit">
|
||||
Upload Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Output,
|
||||
} from 'ng-metadata/core';
|
||||
const templateUrl = require('./load-config.component.html');
|
||||
|
||||
declare var bootbox: any;
|
||||
|
||||
@Component({
|
||||
selector: 'load-config',
|
||||
templateUrl,
|
||||
})
|
||||
export class LoadConfigComponent {
|
||||
private readyToSubmit: boolean = false;
|
||||
private uploadFunc: Function;
|
||||
@Output() public configLoaded: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
private handleTarballSelected(files: File[], callback: Function) {
|
||||
this.readyToSubmit = true;
|
||||
callback(true)
|
||||
}
|
||||
|
||||
private handleTarballCleared() {
|
||||
this.readyToSubmit = false;
|
||||
}
|
||||
|
||||
private uploadTarball() {
|
||||
this.uploadFunc(success => {
|
||||
if (success) {
|
||||
this.configLoaded.emit({});
|
||||
} else {
|
||||
bootbox.dialog({
|
||||
"message": 'Could not upload configuration. Please check you have provided a valid tar file' +
|
||||
'If this problem persists, please contact support',
|
||||
"title": 'Error Loading Configuration',
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When files are validated, this is called by the child to give us
|
||||
* the callback function to upload
|
||||
* @param files: files to upload
|
||||
* @param uploadFiles: function to call to upload files
|
||||
*/
|
||||
private tarballValidatedByUploadBox(files, uploadFiles) {
|
||||
this.uploadFunc = uploadFiles;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
[
|
||||
"javascript",
|
||||
"python",
|
||||
"bash",
|
||||
"nginx",
|
||||
"xml",
|
||||
"shell"
|
||||
]
|
|
@ -0,0 +1,24 @@
|
|||
.markdown-editor-element textarea {
|
||||
height: 300px;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-editor-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.markdown-editor-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.markdown-editor-buttons button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.markdown-editor-buttons button:last-child {
|
||||
margin: 0;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<div class="markdown-editor-element">
|
||||
<!-- Write/preview tabs -->
|
||||
<ul class="nav nav-tabs" style="width: 100%;">
|
||||
<li role="presentation" ng-class="$ctrl.editMode == 'write' ? 'active': ''"
|
||||
ng-click="$ctrl.changeEditMode('write')">
|
||||
<a href="#">Write</a>
|
||||
</li>
|
||||
<li role="presentation" ng-class="$ctrl.editMode == 'preview' ? 'active': ''"
|
||||
ng-click="$ctrl.changeEditMode('preview')">
|
||||
<a href="#">Preview</a>
|
||||
</li>
|
||||
<!-- Editing toolbar -->
|
||||
<li style="float: right;">
|
||||
<markdown-toolbar ng-if="$ctrl.editMode == 'write'"
|
||||
(insert-symbol)="$ctrl.insertSymbol($event)"></markdown-toolbar>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" style="padding: 10px 0 0 0;">
|
||||
<div ng-show="$ctrl.editMode == 'write'">
|
||||
<textarea id="markdown-textarea"
|
||||
placeholder="Enter {{ ::$ctrl.fieldTitle }}"
|
||||
ng-model="$ctrl.content"></textarea>
|
||||
</div>
|
||||
<div class="markdown-editor-preview"
|
||||
ng-if="$ctrl.editMode == 'preview'">
|
||||
<markdown-view content="$ctrl.content"></markdown-view>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="markdown-editor-actions">
|
||||
<div class="markdown-editor-buttons">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-click="$ctrl.discardChanges()">
|
||||
Close
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary"
|
||||
ng-click="$ctrl.saveChanges()">
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,188 @@
|
|||
import { MarkdownEditorComponent, EditMode } from './markdown-editor.component';
|
||||
import { MarkdownSymbol } from '../../../types/common.types';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("MarkdownEditorComponent", () => {
|
||||
var component: MarkdownEditorComponent;
|
||||
var textarea: Mock<ng.IAugmentedJQuery | any>;
|
||||
var documentMock: Mock<HTMLElement & Document>;
|
||||
var $windowMock: Mock<ng.IWindowService>;
|
||||
|
||||
beforeEach(() => {
|
||||
textarea = new Mock<ng.IAugmentedJQuery | any>();
|
||||
documentMock = new Mock<HTMLElement & Document>();
|
||||
$windowMock = new Mock<ng.IWindowService>();
|
||||
const $documentMock: any = [documentMock.Object];
|
||||
component = new MarkdownEditorComponent($documentMock, $windowMock.Object, 'chrome');
|
||||
component.textarea = textarea.Object;
|
||||
});
|
||||
|
||||
describe("onBeforeUnload", () => {
|
||||
|
||||
it("returns false to alert user about losing current changes", () => {
|
||||
component.changeEditMode("write");
|
||||
const allow: boolean = component.onBeforeUnload();
|
||||
|
||||
expect(allow).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnDestroy", () => {
|
||||
|
||||
it("removes 'beforeunload' event listener", () => {
|
||||
$windowMock.setup(mock => mock.onbeforeunload).is(() => 1);
|
||||
component.ngOnDestroy();
|
||||
|
||||
expect($windowMock.Object.onbeforeunload.call(this)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("changeEditMode", () => {
|
||||
|
||||
it("sets component's edit mode to given mode", () => {
|
||||
const editMode: EditMode = "preview";
|
||||
component.changeEditMode(editMode);
|
||||
|
||||
expect(component.currentEditMode).toEqual(editMode);
|
||||
});
|
||||
});
|
||||
|
||||
describe("insertSymbol", () => {
|
||||
var event: {symbol: MarkdownSymbol};
|
||||
var markdownSymbols: {type: MarkdownSymbol, characters: string, shiftBy: number}[];
|
||||
var innerText: string;
|
||||
|
||||
beforeEach(() => {
|
||||
event = {symbol: 'heading1'};
|
||||
innerText = "Here is some text";
|
||||
markdownSymbols = [
|
||||
{type: 'heading1', characters: '# ', shiftBy: 2},
|
||||
{type: 'heading2', characters: '## ', shiftBy: 3},
|
||||
{type: 'heading3', characters: '### ', shiftBy: 4},
|
||||
{type: 'bold', characters: '****', shiftBy: 2},
|
||||
{type: 'italics', characters: '__', shiftBy: 1},
|
||||
{type: 'bulleted-list', characters: '- ', shiftBy: 2},
|
||||
{type: 'numbered-list', characters: '1. ', shiftBy: 3},
|
||||
{type: 'quote', characters: '> ', shiftBy: 2},
|
||||
{type: 'link', characters: '[](url)', shiftBy: 1},
|
||||
{type: 'code', characters: '``', shiftBy: 1},
|
||||
];
|
||||
|
||||
textarea.setup(mock => mock.focus);
|
||||
textarea.setup(mock => mock.substr).is((start, end) => '');
|
||||
textarea.setup(mock => mock.val).is((value?) => innerText);
|
||||
textarea.setup(mock => mock.prop).is((prop) => {
|
||||
switch (prop) {
|
||||
case "selectionStart":
|
||||
return 0;
|
||||
case "selectionEnd":
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
documentMock.setup(mock => mock.execCommand).is((commandID, showUI, value) => false);
|
||||
});
|
||||
|
||||
it("focuses on markdown textarea", () => {
|
||||
component.insertSymbol(event);
|
||||
|
||||
expect(<Spy>textarea.Object.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("inserts correct characters for given symbol at cursor position", () => {
|
||||
markdownSymbols.forEach((symbol) => {
|
||||
event.symbol = symbol.type;
|
||||
component.insertSymbol(event);
|
||||
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[0]).toEqual('insertText');
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[1]).toBe(false);
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[2]).toEqual(symbol.characters);
|
||||
|
||||
(<Spy>documentMock.Object.execCommand).calls.reset();
|
||||
});
|
||||
});
|
||||
|
||||
it("splices highlighted selection between inserted characters instead of deleting them", () => {
|
||||
markdownSymbols.slice(0, 1).forEach((symbol) => {
|
||||
textarea.setup(mock => mock.prop).is((prop) => {
|
||||
switch (prop) {
|
||||
case "selectionStart":
|
||||
return 0;
|
||||
case "selectionEnd":
|
||||
return innerText.length;
|
||||
}
|
||||
});
|
||||
event.symbol = symbol.type;
|
||||
component.insertSymbol(event);
|
||||
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[0]).toEqual('insertText');
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[1]).toBe(false);
|
||||
expect((<Spy>documentMock.Object.execCommand).calls.argsFor(0)[2]).toEqual(`${symbol.characters.slice(0, symbol.shiftBy)}${innerText}${symbol.characters.slice(symbol.shiftBy, symbol.characters.length)}`);
|
||||
|
||||
(<Spy>documentMock.Object.execCommand).calls.reset();
|
||||
});
|
||||
});
|
||||
|
||||
it("moves cursor to correct position for given symbol", () => {
|
||||
markdownSymbols.forEach((symbol) => {
|
||||
event.symbol = symbol.type;
|
||||
component.insertSymbol(event);
|
||||
|
||||
expect((<Spy>textarea.Object.prop).calls.argsFor(2)[0]).toEqual('selectionStart');
|
||||
expect((<Spy>textarea.Object.prop).calls.argsFor(2)[1]).toEqual(symbol.shiftBy);
|
||||
expect((<Spy>textarea.Object.prop).calls.argsFor(3)[0]).toEqual('selectionEnd');
|
||||
expect((<Spy>textarea.Object.prop).calls.argsFor(3)[1]).toEqual(symbol.shiftBy);
|
||||
|
||||
(<Spy>textarea.Object.prop).calls.reset();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("saveChanges", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
component.content = "# Some markdown content";
|
||||
});
|
||||
|
||||
it("emits output event with changed content", (done) => {
|
||||
component.save.subscribe((event: {editedContent: string}) => {
|
||||
expect(event.editedContent).toEqual(component.content);
|
||||
done();
|
||||
});
|
||||
|
||||
component.saveChanges();
|
||||
});
|
||||
});
|
||||
|
||||
describe("discardChanges", () => {
|
||||
|
||||
it("prompts user to confirm discarding changes", () => {
|
||||
const confirmSpy: Spy = $windowMock.setup(mock => mock.confirm).is((message) => false).Spy;
|
||||
component.discardChanges();
|
||||
|
||||
expect(confirmSpy.calls.argsFor(0)[0]).toEqual(`Are you sure you want to discard your changes?`);
|
||||
});
|
||||
|
||||
it("emits output event with no content if user confirms discarding changes", (done) => {
|
||||
$windowMock.setup(mock => mock.confirm).is((message) => true);
|
||||
component.discard.subscribe((event: {}) => {
|
||||
expect(event).toEqual({});
|
||||
done();
|
||||
});
|
||||
|
||||
component.discardChanges();
|
||||
});
|
||||
|
||||
it("does not emit output event if user declines confirmation of discarding changes", (done) => {
|
||||
$windowMock.setup(mock => mock.confirm).is((message) => false);
|
||||
component.discard.subscribe((event: {}) => {
|
||||
fail(`Should not emit output event`);
|
||||
done();
|
||||
});
|
||||
|
||||
component.discardChanges();
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
147
config_app/js/components/markdown/markdown-editor.component.ts
Normal file
147
config_app/js/components/markdown/markdown-editor.component.ts
Normal file
|
@ -0,0 +1,147 @@
|
|||
import { Component, Inject, Input, Output, EventEmitter, ViewChild, HostListener, OnDestroy } from 'ng-metadata/core';
|
||||
import { MarkdownSymbol, BrowserPlatform } from './markdown.module';
|
||||
import './markdown-editor.component.css';
|
||||
|
||||
|
||||
/**
|
||||
* An editing interface for Markdown content.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'markdown-editor',
|
||||
templateUrl: require('./markdown-editor.component.html'),
|
||||
})
|
||||
export class MarkdownEditorComponent implements OnDestroy {
|
||||
|
||||
@Input('<') public content: string;
|
||||
|
||||
@Output() public save: EventEmitter<{editedContent: string}> = new EventEmitter();
|
||||
@Output() public discard: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
// Textarea is public for testability, should not be directly accessed
|
||||
@ViewChild('#markdown-textarea') public textarea: ng.IAugmentedJQuery;
|
||||
|
||||
private editMode: EditMode = "write";
|
||||
|
||||
constructor(@Inject('$document') private $document: ng.IDocumentService,
|
||||
@Inject('$window') private $window: ng.IWindowService,
|
||||
@Inject('BrowserPlatform') private browserPlatform: BrowserPlatform) {
|
||||
this.$window.onbeforeunload = this.onBeforeUnload.bind(this);
|
||||
}
|
||||
|
||||
@HostListener('window:beforeunload', [])
|
||||
public onBeforeUnload(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
this.$window.onbeforeunload = () => null;
|
||||
}
|
||||
|
||||
public changeEditMode(newMode: EditMode): void {
|
||||
this.editMode = newMode;
|
||||
}
|
||||
|
||||
public insertSymbol(event: {symbol: MarkdownSymbol}): void {
|
||||
this.textarea.focus();
|
||||
|
||||
const startPos: number = this.textarea.prop('selectionStart');
|
||||
const endPos: number = this.textarea.prop('selectionEnd');
|
||||
const innerText: string = this.textarea.val().slice(startPos, endPos);
|
||||
var shiftBy: number = 0;
|
||||
var characters: string = '';
|
||||
|
||||
switch (event.symbol) {
|
||||
case 'heading1':
|
||||
characters = '# ';
|
||||
shiftBy = 2;
|
||||
break;
|
||||
case 'heading2':
|
||||
characters = '## ';
|
||||
shiftBy = 3;
|
||||
break;
|
||||
case 'heading3':
|
||||
characters = '### ';
|
||||
shiftBy = 4;
|
||||
break;
|
||||
case 'bold':
|
||||
characters = '****';
|
||||
shiftBy = 2;
|
||||
break;
|
||||
case 'italics':
|
||||
characters = '__';
|
||||
shiftBy = 1;
|
||||
break;
|
||||
case 'bulleted-list':
|
||||
characters = '- ';
|
||||
shiftBy = 2;
|
||||
break;
|
||||
case 'numbered-list':
|
||||
characters = '1. ';
|
||||
shiftBy = 3;
|
||||
break;
|
||||
case 'quote':
|
||||
characters = '> ';
|
||||
shiftBy = 2;
|
||||
break;
|
||||
case 'link':
|
||||
characters = '[](url)';
|
||||
shiftBy = 1;
|
||||
break;
|
||||
case 'code':
|
||||
characters = '``';
|
||||
shiftBy = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
const cursorPos: number = startPos + shiftBy;
|
||||
|
||||
if (startPos != endPos) {
|
||||
this.insertText(`${characters.slice(0, shiftBy)}${innerText}${characters.slice(shiftBy, characters.length)}`,
|
||||
startPos,
|
||||
endPos);
|
||||
}
|
||||
else {
|
||||
this.insertText(characters, startPos, endPos);
|
||||
}
|
||||
|
||||
this.textarea.prop('selectionStart', cursorPos);
|
||||
this.textarea.prop('selectionEnd', cursorPos);
|
||||
}
|
||||
|
||||
public saveChanges(): void {
|
||||
this.save.emit({editedContent: this.content});
|
||||
}
|
||||
|
||||
public discardChanges(): void {
|
||||
if (this.$window.confirm(`Are you sure you want to discard your changes?`)) {
|
||||
this.discard.emit({});
|
||||
}
|
||||
}
|
||||
|
||||
public get currentEditMode(): EditMode {
|
||||
return this.editMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert text in such a way that the browser adds it to the 'undo' stack. This has different feature support
|
||||
* depending on the platform.
|
||||
*/
|
||||
private insertText(text: string, startPos: number, endPos: number): void {
|
||||
if (this.browserPlatform === 'firefox') {
|
||||
// FIXME: Ctrl-Z highlights previous text
|
||||
this.textarea.val(<string>this.textarea.val().substr(0, startPos) +
|
||||
text +
|
||||
<string>this.textarea.val().substr(endPos, this.textarea.val().length));
|
||||
}
|
||||
else {
|
||||
// TODO: Test other platforms (IE...)
|
||||
this.$document[0].execCommand('insertText', false, text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Type representing the current editing mode.
|
||||
*/
|
||||
export type EditMode = "write" | "preview";
|
|
@ -0,0 +1,14 @@
|
|||
.markdown-input-container .glyphicon-edit {
|
||||
float: right;
|
||||
color: #ddd;
|
||||
transition: color 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.markdown-input-container .glyphicon-edit:hover {
|
||||
cursor: pointer;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.markdown-input-placeholder-editable:hover {
|
||||
cursor: pointer;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<div class="markdown-input-container">
|
||||
<div>
|
||||
<span class="glyphicon glyphicon-edit"
|
||||
ng-if="$ctrl.canWrite && !$ctrl.isEditing"
|
||||
ng-click="$ctrl.editContent()"
|
||||
data-title="Edit {{ ::$ctrl.fieldTitle }}" data-placement="left" bs-tooltip></span>
|
||||
<div ng-if="$ctrl.content && !$ctrl.isEditing">
|
||||
<markdown-view content="$ctrl.content"></markdown-view>
|
||||
</div>
|
||||
<!-- Not set and can write -->
|
||||
<span class="markdown-input-placeholder-editable"
|
||||
ng-if="!$ctrl.content && $ctrl.canWrite"
|
||||
ng-click="$ctrl.editContent()">
|
||||
<i>Click to set {{ ::$ctrl.fieldTitle }}</i>
|
||||
</span>
|
||||
<!-- Not set and cannot write -->
|
||||
<span class="markdown-input-placeholder"
|
||||
ng-if="!$ctrl.content && !$ctrl.canWrite">
|
||||
<i>No {{ ::$ctrl.fieldTitle }} has been set</i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Inline editor -->
|
||||
<div ng-if="$ctrl.isEditing" style="margin-top: 20px;">
|
||||
<markdown-editor content="$ctrl.content"
|
||||
(save)="$ctrl.saveContent($event)"
|
||||
(discard)="$ctrl.discardContent($event)"></markdown-editor>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,34 @@
|
|||
import { MarkdownInputComponent } from './markdown-input.component';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("MarkdownInputComponent", () => {
|
||||
var component: MarkdownInputComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
component = new MarkdownInputComponent();
|
||||
});
|
||||
|
||||
describe("editContent", () => {
|
||||
|
||||
});
|
||||
|
||||
describe("saveContent", () => {
|
||||
var editedContent: string;
|
||||
|
||||
it("emits output event with changed content", (done) => {
|
||||
editedContent = "# Some markdown here";
|
||||
component.contentChanged.subscribe((event: {content: string}) => {
|
||||
expect(event.content).toEqual(editedContent);
|
||||
done();
|
||||
});
|
||||
|
||||
component.saveContent({editedContent: editedContent});
|
||||
});
|
||||
});
|
||||
|
||||
describe("discardContent", () => {
|
||||
|
||||
});
|
||||
});
|
|
@ -0,0 +1,34 @@
|
|||
import { Component, Input, Output, EventEmitter } from 'ng-metadata/core';
|
||||
import './markdown-input.component.css';
|
||||
|
||||
|
||||
/**
|
||||
* Displays editable Markdown content.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'markdown-input',
|
||||
templateUrl: require('./markdown-input.component.html'),
|
||||
})
|
||||
export class MarkdownInputComponent {
|
||||
|
||||
@Input('<') public content: string;
|
||||
@Input('<') public canWrite: boolean;
|
||||
@Input('@') public fieldTitle: string;
|
||||
|
||||
@Output() public contentChanged: EventEmitter<{content: string}> = new EventEmitter();
|
||||
|
||||
private isEditing: boolean = false;
|
||||
|
||||
public editContent(): void {
|
||||
this.isEditing = true;
|
||||
}
|
||||
|
||||
public saveContent(event: {editedContent: string}): void {
|
||||
this.contentChanged.emit({content: event.editedContent});
|
||||
this.isEditing = false;
|
||||
}
|
||||
|
||||
public discardContent(event: any): void {
|
||||
this.isEditing = false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
.markdown-toolbar-element .dropdown-menu li > * {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.markdown-toolbar-element .dropdown-menu li:hover {
|
||||
cursor: pointer;
|
||||
background-color: #e6e6e6;
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<div class="markdown-toolbar-element">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group" role="group">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
data-title="Add header" data-container="body" bs-tooltip>
|
||||
<span class="glyphicon glyphicon-text-size"></span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-click="$ctrl.insertSymbol.emit({symbol: 'heading1'})"><h2>Heading</h2></li>
|
||||
<li ng-click="$ctrl.insertSymbol.emit({symbol: 'heading2'})"><h3>Heading</h3></li>
|
||||
<li ng-click="$ctrl.insertSymbol.emit({symbol: 'heading3'})"><h4>Heading</h4></li>
|
||||
</ul>
|
||||
</div>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Bold" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'bold'})">
|
||||
<span class="glyphicon glyphicon-bold"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Italics" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'italics'})">
|
||||
<span class="glyphicon glyphicon-italic"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Block quote" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'quote'})">
|
||||
<i class="fa fa-quote-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Code snippet" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'code'})">
|
||||
<span class="glyphicon glyphicon-menu-left" style="margin-right: -6px;"></span>
|
||||
<span class="glyphicon glyphicon-menu-right"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="URL" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'link'})">
|
||||
<span class="glyphicon glyphicon-link"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Bulleted list" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'bulleted-list'})">
|
||||
<span class="glyphicon glyphicon-list"></span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm"
|
||||
data-title="Numbered list" data-container="body" data-container="body" bs-tooltip
|
||||
ng-click="$ctrl.insertSymbol.emit({symbol: 'numbered-list'})">
|
||||
<i class="fa fa-list-ol" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,11 @@
|
|||
import { MarkdownToolbarComponent } from './markdown-toolbar.component';
|
||||
import { MarkdownSymbol } from '../../../types/common.types';
|
||||
|
||||
|
||||
describe("MarkdownToolbarComponent", () => {
|
||||
var component: MarkdownToolbarComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
component = new MarkdownToolbarComponent();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import { Component, Input, Output, EventEmitter } from 'ng-metadata/core';
|
||||
import { MarkdownSymbol } from './markdown.module';
|
||||
import './markdown-toolbar.component.css';
|
||||
|
||||
|
||||
/**
|
||||
* Toolbar containing Markdown symbol shortcuts.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'markdown-toolbar',
|
||||
templateUrl: require('./markdown-toolbar.component.html'),
|
||||
})
|
||||
export class MarkdownToolbarComponent {
|
||||
|
||||
@Input('<') public allowUndo: boolean = true;
|
||||
@Output() public insertSymbol: EventEmitter<{symbol: MarkdownSymbol}> = new EventEmitter();
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
.markdown-view-content {
|
||||
word-wrap: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.markdown-view-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code * {
|
||||
font-family: "Lucida Console", Monaco, monospace;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<div class="markdown-view-content"
|
||||
ng-bind-html="$ctrl.convertedHTML"></div>
|
|
@ -0,0 +1,79 @@
|
|||
import { MarkdownViewComponent } from './markdown-view.component';
|
||||
import { SimpleChanges } from 'ng-metadata/core';
|
||||
import { Converter, ConverterOptions } from 'showdown';
|
||||
import { Mock } from 'ts-mocks';
|
||||
import Spy = jasmine.Spy;
|
||||
|
||||
|
||||
describe("MarkdownViewComponent", () => {
|
||||
var component: MarkdownViewComponent;
|
||||
var markdownConverterMock: Mock<Converter>;
|
||||
var $sceMock: Mock<ng.ISCEService>;
|
||||
var $sanitizeMock: ng.sanitize.ISanitizeService;
|
||||
|
||||
beforeEach(() => {
|
||||
markdownConverterMock = new Mock<Converter>();
|
||||
$sceMock = new Mock<ng.ISCEService>();
|
||||
$sanitizeMock = jasmine.createSpy('$sanitizeSpy').and.callFake((html: string) => html);
|
||||
component = new MarkdownViewComponent(markdownConverterMock.Object, $sceMock.Object, $sanitizeMock);
|
||||
});
|
||||
|
||||
describe("ngOnChanges", () => {
|
||||
var changes: SimpleChanges;
|
||||
var markdown: string;
|
||||
var expectedPlaceholder: string;
|
||||
var markdownChars: string[];
|
||||
|
||||
beforeEach(() => {
|
||||
changes = {};
|
||||
markdown = `## Heading\n Code line\n\n- Item\n> Quote\`code snippet\`\n\nThis is my project!`;
|
||||
expectedPlaceholder = `<p style="visibility:hidden">placeholder</p>`;
|
||||
markdownChars = ['#', '-', '>', '`'];
|
||||
markdownConverterMock.setup(mock => mock.makeHtml).is((text) => text);
|
||||
$sceMock.setup(mock => mock.trustAsHtml).is((html) => html);
|
||||
});
|
||||
|
||||
it("calls markdown converter to convert content to HTML when content is changed", () => {
|
||||
changes['content'] = {currentValue: markdown, previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>markdownConverterMock.Object.makeHtml).calls.argsFor(0)[0]).toEqual(changes['content'].currentValue);
|
||||
});
|
||||
|
||||
it("only converts first line of content to HTML if flag is set when content is changed", () => {
|
||||
component.firstLineOnly = true;
|
||||
changes['content'] = {currentValue: markdown, previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
const expectedHtml: string = markdown.split('\n')
|
||||
.filter(line => line.indexOf(' ') != 0)
|
||||
.filter(line => line.trim().length != 0)
|
||||
.filter(line => markdownChars.indexOf(line.trim()[0]) == -1)[0];
|
||||
|
||||
expect((<Spy>markdownConverterMock.Object.makeHtml).calls.argsFor(0)[0]).toEqual(expectedHtml);
|
||||
});
|
||||
|
||||
it("sets converted HTML to be a placeholder if flag is set and content is empty", () => {
|
||||
component.placeholderNeeded = true;
|
||||
changes['content'] = {currentValue: '', previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>markdownConverterMock.Object.makeHtml)).not.toHaveBeenCalled();
|
||||
expect((<Spy>$sceMock.Object.trustAsHtml).calls.argsFor(0)[0]).toEqual(expectedPlaceholder);
|
||||
});
|
||||
|
||||
it("sets converted HTML to empty string if placeholder flag is false and content is empty", () => {
|
||||
changes['content'] = {currentValue: '', previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>markdownConverterMock.Object.makeHtml).calls.argsFor(0)[0]).toEqual(changes['content'].currentValue);
|
||||
});
|
||||
|
||||
it("calls $sanitize service to sanitize changed HTML content", () => {
|
||||
changes['content'] = {currentValue: markdown, previousValue: '', isFirstChange: () => false};
|
||||
component.ngOnChanges(changes);
|
||||
|
||||
expect((<Spy>$sanitizeMock).calls.argsFor(0)[0]).toEqual(changes['content'].currentValue);
|
||||
});
|
||||
});
|
||||
});
|
48
config_app/js/components/markdown/markdown-view.component.ts
Normal file
48
config_app/js/components/markdown/markdown-view.component.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Component, Input, Inject, OnChanges, SimpleChanges } from 'ng-metadata/core';
|
||||
import { Converter, ConverterOptions } from 'showdown';
|
||||
import './markdown-view.component.css';
|
||||
|
||||
|
||||
/**
|
||||
* Renders Markdown content to HTML.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'markdown-view',
|
||||
templateUrl: require('./markdown-view.component.html'),
|
||||
})
|
||||
export class MarkdownViewComponent implements OnChanges {
|
||||
|
||||
@Input('<') public content: string;
|
||||
@Input('<') public firstLineOnly: boolean = false;
|
||||
@Input('<') public placeholderNeeded: boolean = false;
|
||||
|
||||
private convertedHTML: string = '';
|
||||
private readonly placeholder: string = `<p style="visibility:hidden">placeholder</p>`;
|
||||
private readonly markdownChars: string[] = ['#', '-', '>', '`'];
|
||||
|
||||
constructor(@Inject('markdownConverter') private markdownConverter: Converter,
|
||||
@Inject('$sce') private $sce: ng.ISCEService,
|
||||
@Inject('$sanitize') private $sanitize: ng.sanitize.ISanitizeService) {
|
||||
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['content']) {
|
||||
if (!changes['content'].currentValue && this.placeholderNeeded) {
|
||||
this.convertedHTML = this.$sce.trustAsHtml(this.placeholder);
|
||||
} else if (this.firstLineOnly && changes['content'].currentValue) {
|
||||
const firstLine: string = changes['content'].currentValue.split('\n')
|
||||
// Skip code lines
|
||||
.filter(line => line.indexOf(' ') != 0)
|
||||
// Skip empty lines
|
||||
.filter(line => line.trim().length != 0)
|
||||
// Skip control lines
|
||||
.filter(line => this.markdownChars.indexOf(line.trim()[0]) == -1)[0];
|
||||
|
||||
this.convertedHTML = this.$sanitize(this.markdownConverter.makeHtml(firstLine));
|
||||
} else {
|
||||
this.convertedHTML = this.$sanitize(this.markdownConverter.makeHtml(changes['content'].currentValue || ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
config_app/js/components/markdown/markdown.module.ts
Normal file
111
config_app/js/components/markdown/markdown.module.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { NgModule } from 'ng-metadata/core';
|
||||
import { Converter } from 'showdown';
|
||||
import * as showdown from 'showdown';
|
||||
import { registerLanguage, highlightAuto } from 'highlight.js/lib/highlight';
|
||||
import 'highlight.js/styles/vs.css';
|
||||
const highlightedLanguages: string[] = require('./highlighted-languages.constant.json');
|
||||
|
||||
/**
|
||||
* A type representing a Markdown symbol.
|
||||
*/
|
||||
export type MarkdownSymbol = 'heading1'
|
||||
| 'heading2'
|
||||
| 'heading3'
|
||||
| 'bold'
|
||||
| 'italics'
|
||||
| 'bulleted-list'
|
||||
| 'numbered-list'
|
||||
| 'quote'
|
||||
| 'code'
|
||||
| 'link'
|
||||
| 'code';
|
||||
|
||||
/**
|
||||
* Type representing current browser platform.
|
||||
* TODO: Add more browser platforms.
|
||||
*/
|
||||
export type BrowserPlatform = "firefox"
|
||||
| "chrome";
|
||||
|
||||
/**
|
||||
* Constant representing current browser platform. Used for determining available features.
|
||||
* TODO Only rudimentary implementation, should prefer specific feature detection strategies instead.
|
||||
*/
|
||||
export const browserPlatform: BrowserPlatform = (() => {
|
||||
if (navigator.userAgent.toLowerCase().indexOf('firefox') != -1) {
|
||||
return 'firefox';
|
||||
}
|
||||
else {
|
||||
return 'chrome';
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Dynamically fetch and register a new language with Highlight.js
|
||||
*/
|
||||
export const addHighlightedLanguage = (language: string): Promise<{}> => {
|
||||
return new Promise(async(resolve, reject) => {
|
||||
try {
|
||||
// TODO: Use `import()` here instead of `require` after upgrading to TypeScript 2.4
|
||||
const langModule = require(`highlight.js/lib/languages/${language}`);
|
||||
registerLanguage(language, langModule);
|
||||
console.debug(`Language ${language} registered for syntax highlighting`);
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.debug(`Language ${language} not supported for syntax highlighting`);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Showdown JS extension for syntax highlighting using Highlight.js. Will attempt to register detected languages.
|
||||
*/
|
||||
export const showdownHighlight = (): showdown.FilterExtension => {
|
||||
const htmlunencode = (text: string) => {
|
||||
return (text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>'));
|
||||
};
|
||||
|
||||
const left = '<pre><code\\b[^>]*>';
|
||||
const right = '</code></pre>';
|
||||
const flags = 'g';
|
||||
const replacement = (wholeMatch: string, match: string, leftSide: string, rightSide: string) => {
|
||||
const language: string = leftSide.slice(leftSide.indexOf('language-') + ('language-').length,
|
||||
leftSide.indexOf('"', leftSide.indexOf('language-')));
|
||||
addHighlightedLanguage(language).catch(error => null);
|
||||
|
||||
match = htmlunencode(match);
|
||||
return leftSide + highlightAuto(match).value + rightSide;
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'output',
|
||||
filter: (text, converter, options) => {
|
||||
return (<any>showdown).helper.replaceRecursiveRegExp(text, replacement, left, right, flags);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
// Import default syntax-highlighting supported languages
|
||||
highlightedLanguages.forEach((langName) => addHighlightedLanguage(langName));
|
||||
|
||||
|
||||
/**
|
||||
* Markdown editor and view module.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [],
|
||||
declarations: [],
|
||||
providers: [
|
||||
{provide: 'markdownConverter', useValue: new Converter({extensions: [<any>showdownHighlight]})},
|
||||
{provide: 'BrowserPlatform', useValue: browserPlatform},
|
||||
],
|
||||
})
|
||||
export class MarkdownModule {
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<span class="registry-name-element">{{ name }}</span>
|
22
config_app/js/components/registry-name/registry-name.js
Normal file
22
config_app/js/components/registry-name/registry-name.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
const templateUrl = require('./registry-name.html');
|
||||
|
||||
/**
|
||||
* An element which displays the name of the registry (optionally the short name).
|
||||
*/
|
||||
angular.module('quay-config').directive('registryName', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl,
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'isShort': '=isShort'
|
||||
},
|
||||
controller: function($scope, $element, Config) {
|
||||
$scope.name = $scope.isShort ? Config.REGISTRY_TITLE_SHORT : Config.REGISTRY_TITLE;
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<div class="request-service-key-dialog-element">
|
||||
<!-- Modal message dialog -->
|
||||
<div class="co-dialog modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-show="!working">×</button>
|
||||
<h4 class="modal-title">Create key for service {{ requestKeyInfo.service }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" ng-show="working">
|
||||
<div class="cor-loader"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" ng-show="!working">
|
||||
<!-- Step 1 (generate) -->
|
||||
<div ng-show="step == 1">
|
||||
<form name="createForm" ng-submit="createPresharedKey()">
|
||||
<table class="co-form-table">
|
||||
<tr>
|
||||
<td><label for="create-key-name">Key Name:</label></td>
|
||||
<td>
|
||||
<input class="form-control" name="create-key-name" type="text" ng-model="preshared.name" placeholder="Friendly Key Name">
|
||||
<span class="co-help-text">
|
||||
A friendly name for the key for later reference.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="create-key-expiration">Expiration date (optional):</label></td>
|
||||
<td>
|
||||
<span class="datetime-picker" datetime="preshared.expiration"></span>
|
||||
<span class="co-help-text">
|
||||
The date and time that the key expires. If left blank, the key will never expire.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="create-key-notes">Approval Notes (optional):</label></td>
|
||||
<td>
|
||||
<markdown-input content="preshared.notes"
|
||||
can-write="true"
|
||||
(content-changed)="updateNotes($event.content)"
|
||||
field-title="notes"></markdown-input>
|
||||
<span class="co-help-text">
|
||||
Optional notes for additional human-readable information about why the key was created.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 (generate) -->
|
||||
<div ng-show="step == 2">
|
||||
<div class="co-alert co-alert-warning">
|
||||
The following key has been generated for service <code>{{ requestKeyInfo.service }}</code>.
|
||||
<br><br>
|
||||
Please copy the key's ID and copy/download the key's private contents and place it in the directory with the service's configuration.
|
||||
<br><br>
|
||||
<strong>Once this dialog is closed this private key will not be accessible anywhere else!</strong>
|
||||
</div>
|
||||
|
||||
<label>Key ID:</label>
|
||||
<div><code>{{ createdKey.kid }}</code></div>
|
||||
<label>Private Key (PEM):</label>
|
||||
<textarea class="key-display form-control" onclick="this.focus();this.select()" readonly>{{ createdKey.private_key }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" ng-show="!working">
|
||||
<button type="button" class="btn btn-primary" ng-show="step == 1"
|
||||
ng-disabled="createForm.$invalid"
|
||||
ng-click="createPresharedKey()">
|
||||
Generate Key
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" ng-click="downloadPrivateKey(createdKey)" ng-if="createdKey && isDownloadSupported()">
|
||||
<i class="fa fa-download"></i> Download Private Key
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step == 2">Close</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-show="step != 2">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,100 @@
|
|||
const templateUrl = require('./request-service-key-dialog.html');
|
||||
/**
|
||||
* An element which displays a dialog for requesting or creating a service key.
|
||||
*/
|
||||
angular.module('quay-config').directive('requestServiceKeyDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl,
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'requestKeyInfo': '=requestKeyInfo',
|
||||
'keyCreated': '&keyCreated'
|
||||
},
|
||||
controller: function($scope, $element, $timeout, ApiService) {
|
||||
var handleNewKey = function(key) {
|
||||
var data = {
|
||||
'notes': 'Approved during setup of service ' + key.service
|
||||
};
|
||||
|
||||
var params = {
|
||||
'kid': key.kid
|
||||
};
|
||||
|
||||
ApiService.approveServiceKey(data, params).then(function(resp) {
|
||||
$scope.keyCreated({'key': key});
|
||||
$scope.step = 2;
|
||||
}, ApiService.errorDisplay('Could not approve service key'));
|
||||
};
|
||||
|
||||
$scope.show = function() {
|
||||
$scope.working = false;
|
||||
$scope.step = 1;
|
||||
|
||||
var notes = 'Created during setup for service `' + $scope.requestKeyInfo.service + '`';
|
||||
if ($scope.requestKeyInfo.newKey) {
|
||||
notes = 'Replacement key for service `' + $scope.requestKeyInfo.service + '`';
|
||||
}
|
||||
|
||||
$scope.preshared = {
|
||||
'name': $scope.requestKeyInfo.service + ' Service Key',
|
||||
'notes': notes
|
||||
};
|
||||
|
||||
$element.find('.modal').modal({});
|
||||
};
|
||||
|
||||
$scope.hide = function() {
|
||||
$scope.loading = false;
|
||||
$element.find('.modal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.isDownloadSupported = function() {
|
||||
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
|
||||
if (isSafari) {
|
||||
// Doesn't work properly in Safari, sadly.
|
||||
return false;
|
||||
}
|
||||
|
||||
try { return !!new Blob(); } catch(e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.downloadPrivateKey = function(key) {
|
||||
var blob = new Blob([key.private_key]);
|
||||
FileSaver.saveAs(blob, key.service + '.pem');
|
||||
};
|
||||
|
||||
$scope.createPresharedKey = function() {
|
||||
$scope.working = true;
|
||||
|
||||
var data = {
|
||||
'name': $scope.preshared.name,
|
||||
'service': $scope.requestKeyInfo.service,
|
||||
'expiration': $scope.preshared.expiration || null,
|
||||
'notes': $scope.preshared.notes
|
||||
};
|
||||
|
||||
ApiService.createServiceKey(data).then(function(resp) {
|
||||
$scope.working = false;
|
||||
$scope.step = 2;
|
||||
$scope.createdKey = resp;
|
||||
$scope.keyCreated({'key': resp});
|
||||
}, ApiService.errorDisplay('Could not create service key'));
|
||||
};
|
||||
|
||||
$scope.updateNotes = function(content) {
|
||||
$scope.preshared.notes = content;
|
||||
};
|
||||
|
||||
$scope.$watch('requestKeyInfo', function(info) {
|
||||
if (info && info.service) {
|
||||
$scope.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
65
config_app/js/config-app.module.ts
Normal file
65
config_app/js/config-app.module.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { NgModule } from 'ng-metadata/core';
|
||||
import * as restangular from 'restangular';
|
||||
|
||||
import { ConfigSetupAppComponent } from './components/config-setup-app/config-setup-app.component';
|
||||
import { DownloadTarballModalComponent } from './components/download-tarball-modal/download-tarball-modal.component';
|
||||
import { LoadConfigComponent } from './components/load-config/load-config.component';
|
||||
import { KubeDeployModalComponent } from './components/kube-deploy-modal/kube-deploy-modal.component';
|
||||
import { MarkdownModule } from './components/markdown/markdown.module';
|
||||
import { MarkdownInputComponent } from './components/markdown/markdown-input.component';
|
||||
import { MarkdownViewComponent } from './components/markdown/markdown-view.component';
|
||||
import { MarkdownToolbarComponent } from './components/markdown/markdown-toolbar.component';
|
||||
import { MarkdownEditorComponent } from './components/markdown/markdown-editor.component';
|
||||
|
||||
const quayDependencies: any[] = [
|
||||
'restangular',
|
||||
'ngCookies',
|
||||
'angularFileUpload',
|
||||
'ngSanitize',
|
||||
];
|
||||
|
||||
@NgModule(({
|
||||
imports: quayDependencies,
|
||||
declarations: [],
|
||||
providers: [
|
||||
provideConfig,
|
||||
]
|
||||
}))
|
||||
class DependencyConfig{}
|
||||
|
||||
|
||||
provideConfig.$inject = [
|
||||
'$provide',
|
||||
'$injector',
|
||||
'$compileProvider',
|
||||
'RestangularProvider',
|
||||
];
|
||||
|
||||
function provideConfig($provide: ng.auto.IProvideService,
|
||||
$injector: ng.auto.IInjectorService,
|
||||
$compileProvider: ng.ICompileProvider,
|
||||
RestangularProvider: any): void {
|
||||
|
||||
// Configure the API provider.
|
||||
RestangularProvider.setBaseUrl('/api/v1/');
|
||||
}
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
DependencyConfig,
|
||||
MarkdownModule,
|
||||
],
|
||||
declarations: [
|
||||
ConfigSetupAppComponent,
|
||||
DownloadTarballModalComponent,
|
||||
LoadConfigComponent,
|
||||
KubeDeployModalComponent,
|
||||
MarkdownInputComponent,
|
||||
MarkdownViewComponent,
|
||||
MarkdownToolbarComponent,
|
||||
MarkdownEditorComponent,
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
export class ConfigAppModule {}
|
|
@ -0,0 +1,8 @@
|
|||
<div class="config-bool-field-element">
|
||||
<form name="fieldform" novalidate>
|
||||
<label>
|
||||
<input type="checkbox" ng-model="binding">
|
||||
<span ng-transclude/>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,77 @@
|
|||
<div class="config-certificates-field-element">
|
||||
<div class="resource-view" resource="certificatesResource" error-message="'Could not load certificates list'">
|
||||
<!-- File -->
|
||||
<div class="co-alert co-alert-warning" ng-if="certInfo.status == 'file'">
|
||||
<code>extra_ca_certs</code> is a single file and cannot be processed by this tool. If a valid and appended list of certificates, they will be installed on container startup.
|
||||
</div>
|
||||
|
||||
<div ng-if="certInfo.status != 'file'">
|
||||
<div class="description">
|
||||
<p>This section lists any custom or self-signed SSL certificates that are installed in the <span class="registry-name"></span> container on startup after being read from the <code>extra_ca_certs</code> directory in the configuration volume.
|
||||
</p>
|
||||
<p>
|
||||
Custom certificates are typically used in place of publicly signed certificates for corporate-internal services.
|
||||
</p>
|
||||
<p>Please <strong>make sure</strong> that all custom names used for downstream services (such as Clair) are listed in the certificates below.</p>
|
||||
</div>
|
||||
|
||||
<table class="config-table" style="margin-bottom: 20px;">
|
||||
<tr>
|
||||
<td>Upload certificates:</td>
|
||||
<td>
|
||||
<div class="file-upload-box"
|
||||
api-endpoint="superuser/customcerts"
|
||||
select-message="Select custom certificate to add to configuration. Must be in PEM format and end extension '.crt'"
|
||||
files-selected="handleCertsSelected(files, callback)"
|
||||
reset="resetUpload"
|
||||
extensions="['.crt']"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table class="co-table">
|
||||
<thead>
|
||||
<td>Certificate Filename</td>
|
||||
<td>Status</td>
|
||||
<td>Names Handled</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
<tr ng-repeat="certificate in certInfo.certs" ng-if="!certsUploading">
|
||||
<td>{{ certificate.path }}</td>
|
||||
<td class="cert-status">
|
||||
<div ng-if="certificate.error" class="red">
|
||||
<i class="fa fa-exclamation-circle"></i>
|
||||
Error: {{ certificate.error }}
|
||||
</div>
|
||||
<div ng-if="certificate.expired" class="orange">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
Certificate is expired
|
||||
</div>
|
||||
<div ng-if="!certificate.error && !certificate.expired" class="green">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
Certificate is valid
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="empty" ng-if="!certificate.names">(None)</div>
|
||||
<a class="dns-name" ng-href="http://{{ name }}" ng-repeat="name in certificate.names" ng-safenewtab>{{ name }}</a>
|
||||
</td>
|
||||
<td class="options-col">
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="deleteCert(certificate.path)">
|
||||
<i class="fa fa-times"></i> Delete Certificate
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div ng-if="certsUploading" style="margin-top: 20px; text-align: center;">
|
||||
<div class="cor-loader-inline"></div>
|
||||
Uploading, validating and updating certificate(s)
|
||||
</div>
|
||||
<div class="empty" ng-if="!certInfo.certs.length && !certsUploading" style="margin-top: 20px;">
|
||||
<div class="empty-primary-msg">No custom certificates found.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,46 @@
|
|||
<div class="config-contact-field-element">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
|
||||
<span ng-switch="kind">
|
||||
<span ng-switch-when="mailto"><i class="fa fa-envelope"></i>E-mail</span>
|
||||
<span ng-switch-when="irc"><i class="fa fa-comment"></i>IRC</span>
|
||||
<span ng-switch-when="tel"><i class="fa fa-phone"></i>Phone</span>
|
||||
<span ng-switch-default><i class="fa fa-ticket-alt"></i>URL</span>
|
||||
</span>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" ng-click="kind = 'mailto'">
|
||||
<i class="fa fa-envelope"></i> E-mail
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" ng-click="kind = 'irc'">
|
||||
<i class="fa fa-comment"></i> IRC
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" ng-click="kind = 'tel'">
|
||||
<i class="fa fa-phone"></i> Telephone
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a role="menuitem" tabindex="-1" ng-click="kind = 'http'">
|
||||
<i class="fa fa-ticket-alt"></i> URL
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<form>
|
||||
<input class="form-control" placeholder="{{ getPlaceholder(kind) }}" ng-model="value">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
<div class="config-contacts-field-element">
|
||||
<div class="config-contact-field" binding="item.value" ng-repeat="item in items">
|
||||
</div>
|
||||
</div>
|
13
config_app/js/config-field-templates/config-file-field.html
Normal file
13
config_app/js/config-field-templates/config-file-field.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="config-file-field-element">
|
||||
<span ng-show="uploadProgress == null">
|
||||
<span ng-if="hasFile">
|
||||
<code>/conf/stack/{{ filename }}</code>
|
||||
<span style="margin-left: 20px; display: inline-block;">Select a replacement file:</span>
|
||||
</span>
|
||||
<span class="nofile" ng-if="!hasFile && skipCheckFile != 'true'">Please select a file to upload as <b>{{ filename }}</b>: </span>
|
||||
<input type="file" ng-file-select="onFileSelect($files)">
|
||||
</span>
|
||||
<span ng-show="uploadProgress != null">
|
||||
Uploading file as <strong>{{ filename }}</strong>... {{ uploadProgress }}%
|
||||
</span>
|
||||
</div>
|
17
config_app/js/config-field-templates/config-list-field.html
Normal file
17
config_app/js/config-field-templates/config-list-field.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div class="config-list-field-element">
|
||||
<ul ng-show="binding && binding.length">
|
||||
<li class="item" ng-repeat="item in binding">
|
||||
<span class="item-title">{{ item }}</span>
|
||||
<span class="item-delete">
|
||||
<a ng-click="removeItem(item)">Remove</a>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<span class="empty" ng-if="!binding || binding.length == 0">No {{ itemTitle }}s defined</span>
|
||||
<form class="form-control-container" ng-submit="addItem()">
|
||||
<input type="text" class="form-control" placeholder="{{ placeholder }}"
|
||||
ng-pattern="getRegexp(itemPattern)"
|
||||
ng-model="newItemName" style="display: inline-block">
|
||||
<button class="btn btn-default" style="display: inline-block">Add</button>
|
||||
</form>
|
||||
</div>
|
20
config_app/js/config-field-templates/config-map-field.html
Normal file
20
config_app/js/config-field-templates/config-map-field.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<div class="config-map-field-element">
|
||||
<table class="table" ng-show="hasValues(binding)">
|
||||
<tr class="item" ng-repeat="(key, value) in binding">
|
||||
<td class="item-title">{{ key }}</td>
|
||||
<td class="item-value">{{ value }}</td>
|
||||
<td class="item-delete">
|
||||
<a ng-click="removeKey(key)">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<span class="empty" ng-if="!hasValues(binding)">No entries defined</span>
|
||||
<form class="form-control-container" ng-submit="addEntry()">
|
||||
Add Key-Value:
|
||||
<select ng-model="newKey">
|
||||
<option ng-repeat="key in keys" value="{{ key }}">{{ key }}</option>
|
||||
</select>
|
||||
<input type="text" class="form-control" placeholder="Value" ng-model="newValue">
|
||||
<button class="btn btn-default" style="display: inline-block">Add Entry</button>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="config-numeric-field-element">
|
||||
<form name="fieldform" novalidate>
|
||||
<input type="number" class="form-control" placeholder="{{ placeholder || '' }}"
|
||||
ng-model="bindinginternal" ng-trim="false" ng-minlength="1" required>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1 @@
|
|||
<div class="config-parsed-field-element"></div>
|
|
@ -0,0 +1,9 @@
|
|||
<div class="config-password-field-element">
|
||||
<form name="fieldform" novalidate>
|
||||
<input type="password" class="form-control" placeholder="{{ placeholder || '' }}"
|
||||
ng-model="binding" ng-trim="false" ng-minlength="1" ng-required="!isOptional">
|
||||
<div class="alert alert-danger" ng-show="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
<div class="config-service-key-field-element">
|
||||
<!-- Loading -->
|
||||
<div class="cor-loader" ng-if="loading"></div>
|
||||
|
||||
<!-- Loading error -->
|
||||
<div class="co-alert co-alert-warning" ng-if="loadError">
|
||||
Could not load service keys
|
||||
</div>
|
||||
|
||||
<!-- Key config -->
|
||||
<div ng-show="!loading && !loadError">
|
||||
<div ng-show="hasValidKey">
|
||||
<i class="fa fa-check"></i>
|
||||
Valid key for service <code>{{ serviceName }}</code> exists
|
||||
<a class="co-modify-link" ng-click="showRequestServiceKey(true)">Assign New Key</a>
|
||||
</div>
|
||||
<div ng-show="!hasValidKey">
|
||||
No valid key found for service <code>{{ serviceName }}</code>
|
||||
<a class="co-modify-link" ng-click="showRequestServiceKey()">Create Key</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note: This field is a hidden text field that binds to a model that is set to non-empty
|
||||
when there is a valid key. This allows us to use the existing Angular form validation
|
||||
code.
|
||||
-->
|
||||
<input type="text" ng-model="hasValidKeyStr" ng-required="true" style="position: absolute; top: 0px; left: 0px; visibility: hidden; width: 0px; height: 0px;">
|
||||
|
||||
<div class="request-service-key-dialog" request-key-info="requestKeyInfo" key-created="handleKeyCreated(key)"></div>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div class="config-string-field-element">
|
||||
<form name="fieldform" novalidate>
|
||||
<input type="text" class="form-control" placeholder="{{ placeholder || '' }}"
|
||||
ng-model="binding" ng-trim="false" ng-minlength="1"
|
||||
ng-pattern="getRegexp(pattern)" ng-required="!isOptional">
|
||||
<div class="alert alert-danger" ng-show="errorMessage">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<div class="config-string-list-field-element">
|
||||
<form name="fieldform" novalidate>
|
||||
<input type="text" class="form-control" placeholder="{{ placeholder || '' }}"
|
||||
ng-model="internalBinding" ng-trim="true" ng-minlength="1" ng-required="!isOptional">
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,10 @@
|
|||
<div class="config-variable-field-element">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default"
|
||||
ng-repeat="section in sections"
|
||||
ng-click="setSection(section)"
|
||||
ng-class="section == currentSection ? 'active' : ''">{{ section.title }}</button>
|
||||
</div>
|
||||
|
||||
<span ng-transclude></span>
|
||||
</div>
|
1861
config_app/js/core-config-setup/config-setup-tool.html
Normal file
1861
config_app/js/core-config-setup/config-setup-tool.html
Normal file
File diff suppressed because it is too large
Load diff
1454
config_app/js/core-config-setup/core-config-setup.js
Normal file
1454
config_app/js/core-config-setup/core-config-setup.js
Normal file
File diff suppressed because it is too large
Load diff
37
config_app/js/main.ts
Normal file
37
config_app/js/main.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
// imports shims, etc
|
||||
import 'core-js';
|
||||
|
||||
import * as angular from 'angular';
|
||||
import { ConfigAppModule } from './config-app.module';
|
||||
import { bundle } from 'ng-metadata/core';
|
||||
|
||||
// load all app dependencies
|
||||
require('../static/lib/angular-file-upload.min.js');
|
||||
require('../../static/js/tar');
|
||||
|
||||
const ng1QuayModule: string = bundle(ConfigAppModule, []).name;
|
||||
angular.module('quay-config', [ng1QuayModule])
|
||||
.run(() => {
|
||||
});
|
||||
|
||||
declare var require: any;
|
||||
function requireAll(r) {
|
||||
r.keys().forEach(r);
|
||||
}
|
||||
|
||||
// load all services
|
||||
requireAll(require.context('./services', true, /\.js$/));
|
||||
|
||||
|
||||
// load all the components after services
|
||||
requireAll(require.context('./setup', true, /\.js$/));
|
||||
requireAll(require.context('./core-config-setup', true, /\.js$/));
|
||||
requireAll(require.context('./components', true, /\.js$/));
|
||||
|
||||
// load config-app specific css
|
||||
requireAll(require.context('../static/css', true, /\.css$/));
|
||||
|
||||
|
||||
// Load all the main quay css
|
||||
requireAll(require.context('../../static/css', true, /\.css$/));
|
||||
requireAll(require.context('../../static/lib', true, /\.css$/));
|
107
config_app/js/services/angular-poll-channel.js
vendored
Normal file
107
config_app/js/services/angular-poll-channel.js
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* Specialized class for conducting an HTTP poll, while properly preventing multiple calls.
|
||||
*/
|
||||
angular.module('quay-config').factory('AngularPollChannel',
|
||||
['ApiService', '$timeout', 'DocumentVisibilityService', 'CORE_EVENT', '$rootScope',
|
||||
function(ApiService, $timeout, DocumentVisibilityService, CORE_EVENT, $rootScope) {
|
||||
var _PollChannel = function(scope, requester, opt_sleeptime) {
|
||||
this.scope_ = scope;
|
||||
this.requester_ = requester;
|
||||
this.sleeptime_ = opt_sleeptime || (60 * 1000 /* 60s */);
|
||||
this.timer_ = null;
|
||||
|
||||
this.working = false;
|
||||
this.polling = false;
|
||||
this.skipping = false;
|
||||
|
||||
var that = this;
|
||||
|
||||
var visibilityHandler = $rootScope.$on(CORE_EVENT.DOC_VISIBILITY_CHANGE, function() {
|
||||
// If the poll channel was skipping because the visibility was hidden, call it immediately.
|
||||
if (that.skipping && !DocumentVisibilityService.isHidden()) {
|
||||
that.call_();
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
that.stop();
|
||||
visibilityHandler();
|
||||
});
|
||||
};
|
||||
|
||||
_PollChannel.prototype.setSleepTime = function(sleepTime) {
|
||||
this.sleeptime_ = sleepTime;
|
||||
this.stop();
|
||||
this.start(true);
|
||||
};
|
||||
|
||||
_PollChannel.prototype.stop = function() {
|
||||
if (this.timer_) {
|
||||
$timeout.cancel(this.timer_);
|
||||
this.timer_ = null;
|
||||
this.polling = false;
|
||||
}
|
||||
|
||||
this.skipping = false;
|
||||
this.working = false;
|
||||
};
|
||||
|
||||
_PollChannel.prototype.start = function(opt_skipFirstCall) {
|
||||
// Make sure we invoke call outside the normal digest cycle, since
|
||||
// we'll call $scope.$apply ourselves.
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
if (opt_skipFirstCall) {
|
||||
that.setupTimer_();
|
||||
return;
|
||||
}
|
||||
|
||||
that.call_();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
_PollChannel.prototype.call_ = function() {
|
||||
if (this.working) { return; }
|
||||
|
||||
// If the document is currently hidden, skip the call.
|
||||
if (DocumentVisibilityService.isHidden()) {
|
||||
this.skipping = true;
|
||||
this.setupTimer_();
|
||||
return;
|
||||
}
|
||||
|
||||
var that = this;
|
||||
this.working = true;
|
||||
|
||||
$timeout(function() {
|
||||
that.requester_(function(status) {
|
||||
if (status) {
|
||||
that.working = false;
|
||||
that.skipping = false;
|
||||
that.setupTimer_();
|
||||
} else {
|
||||
that.stop();
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
_PollChannel.prototype.setupTimer_ = function() {
|
||||
if (this.timer_) { return; }
|
||||
|
||||
var that = this;
|
||||
this.polling = true;
|
||||
this.timer_ = $timeout(function() {
|
||||
that.timer_ = null;
|
||||
that.call_();
|
||||
}, this.sleeptime_)
|
||||
};
|
||||
|
||||
var service = {
|
||||
'create': function(scope, requester, opt_sleeptime) {
|
||||
return new _PollChannel(scope, requester, opt_sleeptime);
|
||||
}
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
335
config_app/js/services/api-service.js
Normal file
335
config_app/js/services/api-service.js
Normal file
|
@ -0,0 +1,335 @@
|
|||
/**
|
||||
* Service which exposes the server-defined API as a nice set of helper methods and automatic
|
||||
* callbacks. Any method defined on the server is exposed here as an equivalent method. Also
|
||||
* defines some helper functions for working with API responses.
|
||||
*/
|
||||
angular.module('quay-config').factory('ApiService', ['Restangular', '$q', 'UtilService', function(Restangular, $q, UtilService) {
|
||||
var apiService = {};
|
||||
|
||||
if (!window.__endpoints) {
|
||||
return apiService;
|
||||
}
|
||||
|
||||
var getResource = function(getMethod, operation, opt_parameters, opt_background) {
|
||||
var resource = {};
|
||||
resource.withOptions = function(options) {
|
||||
this.options = options;
|
||||
return this;
|
||||
};
|
||||
|
||||
resource.get = function(processor, opt_errorHandler) {
|
||||
var options = this.options;
|
||||
var result = {
|
||||
'loading': true,
|
||||
'value': null,
|
||||
'hasError': false
|
||||
};
|
||||
|
||||
getMethod(options, opt_parameters, opt_background, true).then(function(resp) {
|
||||
result.value = processor(resp);
|
||||
result.loading = false;
|
||||
}, function(resp) {
|
||||
result.hasError = true;
|
||||
result.loading = false;
|
||||
if (opt_errorHandler) {
|
||||
opt_errorHandler(resp);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return resource;
|
||||
};
|
||||
|
||||
var buildUrl = function(path, parameters) {
|
||||
// We already have /api/v1/ on the URLs, so remove them from the paths.
|
||||
path = path.substr('/api/v1/'.length, path.length);
|
||||
|
||||
// Build the path, adjusted with the inline parameters.
|
||||
var used = {};
|
||||
var url = '';
|
||||
for (var i = 0; i < path.length; ++i) {
|
||||
var c = path[i];
|
||||
if (c == '{') {
|
||||
var end = path.indexOf('}', i);
|
||||
var varName = path.substr(i + 1, end - i - 1);
|
||||
|
||||
if (!parameters[varName]) {
|
||||
throw new Error('Missing parameter: ' + varName);
|
||||
}
|
||||
|
||||
used[varName] = true;
|
||||
url += parameters[varName];
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
url += c;
|
||||
}
|
||||
|
||||
// Append any query parameters.
|
||||
var isFirst = true;
|
||||
for (var paramName in parameters) {
|
||||
if (!parameters.hasOwnProperty(paramName)) { continue; }
|
||||
if (used[paramName]) { continue; }
|
||||
|
||||
var value = parameters[paramName];
|
||||
if (value) {
|
||||
url += isFirst ? '?' : '&';
|
||||
url += paramName + '=' + encodeURIComponent(value)
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
var getGenericOperationName = function(userOperationName) {
|
||||
return userOperationName.replace('User', '');
|
||||
};
|
||||
|
||||
var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) {
|
||||
if (userRelatedResource) {
|
||||
if (userRelatedResource[method.toLowerCase()]) {
|
||||
return userRelatedResource[method.toLowerCase()]['operationId'];
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Could not find user operation matching org operation: ' + orgOperationName);
|
||||
};
|
||||
|
||||
var freshLoginInProgress = [];
|
||||
var reject = function(msg) {
|
||||
for (var i = 0; i < freshLoginInProgress.length; ++i) {
|
||||
freshLoginInProgress[i].deferred.reject({'data': {'message': msg}});
|
||||
}
|
||||
freshLoginInProgress = [];
|
||||
};
|
||||
|
||||
var retry = function() {
|
||||
for (var i = 0; i < freshLoginInProgress.length; ++i) {
|
||||
freshLoginInProgress[i].retry();
|
||||
}
|
||||
freshLoginInProgress = [];
|
||||
};
|
||||
|
||||
var freshLoginFailCheck = function(opName, opArgs) {
|
||||
return function(resp) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
// If the error is a fresh login required, show the dialog.
|
||||
// TODO: remove error_type (old style error)
|
||||
var fresh_login_required = resp.data['title'] == 'fresh_login_required' || resp.data['error_type'] == 'fresh_login_required';
|
||||
if (resp.status == 401 && fresh_login_required) {
|
||||
var retryOperation = function() {
|
||||
apiService[opName].apply(apiService, opArgs).then(function(resp) {
|
||||
deferred.resolve(resp);
|
||||
}, function(resp) {
|
||||
deferred.reject(resp);
|
||||
});
|
||||
};
|
||||
|
||||
var verifyNow = function() {
|
||||
if (!$('#freshPassword').val()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var info = {
|
||||
'password': $('#freshPassword').val()
|
||||
};
|
||||
|
||||
$('#freshPassword').val('');
|
||||
|
||||
// Conduct the sign in of the user.
|
||||
apiService.verifyUser(info).then(function() {
|
||||
// On success, retry the operations. if it succeeds, then resolve the
|
||||
// deferred promise with the result. Otherwise, reject the same.
|
||||
retry();
|
||||
}, function(resp) {
|
||||
// Reject with the sign in error.
|
||||
reject('Invalid verification credentials');
|
||||
});
|
||||
};
|
||||
|
||||
// Add the retry call to the in progress list. If there is more than a single
|
||||
// in progress call, we skip showing the dialog (since it has already been
|
||||
// shown).
|
||||
freshLoginInProgress.push({
|
||||
'deferred': deferred,
|
||||
'retry': retryOperation
|
||||
})
|
||||
|
||||
if (freshLoginInProgress.length > 1) {
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
var box = bootbox.dialog({
|
||||
"message": 'It has been more than a few minutes since you last logged in, ' +
|
||||
'so please verify your password to perform this sensitive operation:' +
|
||||
'<form style="margin-top: 10px" action="javascript:$(\'.btn-continue\').click();void(0)">' +
|
||||
'<input id="freshPassword" class="form-control" type="password" placeholder="Current Password">' +
|
||||
'</form>',
|
||||
"title": 'Please Verify',
|
||||
"buttons": {
|
||||
"verify": {
|
||||
"label": "Verify",
|
||||
"className": "btn-success btn-continue",
|
||||
"callback": verifyNow
|
||||
},
|
||||
"close": {
|
||||
"label": "Cancel",
|
||||
"className": "btn-default",
|
||||
"callback": function() {
|
||||
reject('Verification canceled')
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
box.bind('shown.bs.modal', function(){
|
||||
box.find("input").focus();
|
||||
box.find("form").submit(function() {
|
||||
if (!$('#freshPassword').val()) { return; }
|
||||
|
||||
box.modal('hide');
|
||||
verifyNow();
|
||||
});
|
||||
});
|
||||
|
||||
// Return a new promise. We'll accept or reject it based on the result
|
||||
// of the login.
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Otherwise, we just 'raise' the error via the reject method on the promise.
|
||||
return $q.reject(resp);
|
||||
};
|
||||
};
|
||||
|
||||
var buildMethodsForOperation = function(operation, method, path, resourceMap) {
|
||||
var operationName = operation['operationId'];
|
||||
var urlPath = path['x-path'];
|
||||
|
||||
// Add the operation itself.
|
||||
apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forceget, opt_responseType) {
|
||||
var one = Restangular.one(buildUrl(urlPath, opt_parameters));
|
||||
|
||||
if (opt_background || opt_responseType) {
|
||||
let httpConfig = {};
|
||||
|
||||
if (opt_background) {
|
||||
httpConfig['ignoreLoadingBar'] = true;
|
||||
}
|
||||
if (opt_responseType) {
|
||||
httpConfig['responseType'] = opt_responseType;
|
||||
}
|
||||
|
||||
one.withHttpConfig(httpConfig);
|
||||
}
|
||||
|
||||
var opObj = one[opt_forceget ? 'get' : 'custom' + method.toUpperCase()](opt_options);
|
||||
|
||||
// If the operation requires_fresh_login, then add a specialized error handler that
|
||||
// will defer the operation's result if sudo is requested.
|
||||
if (operation['x-requires-fresh-login']) {
|
||||
opObj = opObj.catch(freshLoginFailCheck(operationName, arguments));
|
||||
}
|
||||
return opObj;
|
||||
};
|
||||
|
||||
// If the method for the operation is a GET, add an operationAsResource method.
|
||||
if (method == 'get') {
|
||||
apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) {
|
||||
var getMethod = apiService[operationName];
|
||||
return getResource(getMethod, operation, opt_parameters, opt_background);
|
||||
};
|
||||
}
|
||||
|
||||
// If the operation has a user-related operation, then make a generic operation for this operation
|
||||
// that can call both the user and the organization versions of the operation, depending on the
|
||||
// parameters given.
|
||||
if (path['x-user-related']) {
|
||||
var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[path['x-user-related']]);
|
||||
var genericOperationName = getGenericOperationName(userOperationName);
|
||||
apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) {
|
||||
if (orgname) {
|
||||
if (orgname.name) {
|
||||
orgname = orgname.name;
|
||||
}
|
||||
|
||||
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background);
|
||||
return apiService[operationName](opt_options, params);
|
||||
} else {
|
||||
return apiService[userOperationName](opt_options, opt_parameters, opt_background);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var allowedMethods = ['get', 'post', 'put', 'delete'];
|
||||
var resourceMap = {};
|
||||
var forEachOperation = function(callback) {
|
||||
for (var path in window.__endpoints) {
|
||||
if (!window.__endpoints.hasOwnProperty(path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var method in window.__endpoints[path]) {
|
||||
if (!window.__endpoints[path].hasOwnProperty(method)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allowedMethods.indexOf(method.toLowerCase()) < 0) { continue; }
|
||||
callback(window.__endpoints[path][method], method, window.__endpoints[path]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Build the map of resource names to their objects.
|
||||
forEachOperation(function(operation, method, path) {
|
||||
resourceMap[path['x-name']] = path;
|
||||
});
|
||||
|
||||
// Construct the methods for each API endpoint.
|
||||
forEachOperation(function(operation, method, path) {
|
||||
buildMethodsForOperation(operation, method, path, resourceMap);
|
||||
});
|
||||
|
||||
apiService.getErrorMessage = function(resp, defaultMessage) {
|
||||
var message = defaultMessage;
|
||||
if (resp && resp['data']) {
|
||||
//TODO: remove error_message and error_description (old style error)
|
||||
message = resp['data']['detail'] || resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
apiService.errorDisplay = function(defaultMessage, opt_handler) {
|
||||
return function(resp) {
|
||||
var message = apiService.getErrorMessage(resp, defaultMessage);
|
||||
if (opt_handler) {
|
||||
var handlerMessage = opt_handler(resp);
|
||||
if (handlerMessage) {
|
||||
message = handlerMessage;
|
||||
}
|
||||
}
|
||||
|
||||
message = UtilService.stringToHTML(message);
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": defaultMessage || 'Request Failure',
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return apiService;
|
||||
}]);
|
43
config_app/js/services/container-service.js
Normal file
43
config_app/js/services/container-service.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Helper service for working with the registry's container. Only works in enterprise.
|
||||
*/
|
||||
angular.module('quay-config')
|
||||
.factory('ContainerService', ['ApiService', '$timeout', 'Restangular',
|
||||
function(ApiService, $timeout, Restangular) {
|
||||
var containerService = {};
|
||||
containerService.restartContainer = function(callback) {
|
||||
ApiService.errorDisplay('Removed Endpoint. This error should never be seen.')
|
||||
};
|
||||
|
||||
containerService.scheduleStatusCheck = function(callback, opt_config) {
|
||||
$timeout(function() {
|
||||
containerService.checkStatus(callback, opt_config);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
containerService.checkStatus = function(callback, opt_config) {
|
||||
var errorHandler = function(resp) {
|
||||
if (resp.status == 404 || resp.status == 502 || resp.status == -1) {
|
||||
// Container has not yet come back up, so we schedule another check.
|
||||
containerService.scheduleStatusCheck(callback, opt_config);
|
||||
return;
|
||||
}
|
||||
|
||||
return ApiService.errorDisplay('Cannot load status. Please report this to support')(resp);
|
||||
};
|
||||
|
||||
// If config is specified, override the API base URL from this point onward.
|
||||
// TODO: Find a better way than this. This is safe, since this will only be called
|
||||
// for a restart, but it is still ugly.
|
||||
if (opt_config && opt_config['SERVER_HOSTNAME']) {
|
||||
var scheme = opt_config['PREFERRED_URL_SCHEME'] || 'http';
|
||||
var baseUrl = scheme + '://' + opt_config['SERVER_HOSTNAME'] + '/api/v1/';
|
||||
Restangular.setBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
ApiService.scRegistryStatus(null, null, /* background */true)
|
||||
.then(callback, errorHandler);
|
||||
};
|
||||
|
||||
return containerService;
|
||||
}]);
|
23
config_app/js/services/cookie-service.js
Normal file
23
config_app/js/services/cookie-service.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Helper service for working with cookies.
|
||||
*/
|
||||
angular.module('quay-config').factory('CookieService', ['$cookies', function($cookies) {
|
||||
var cookieService = {};
|
||||
cookieService.putPermanent = function(name, value) {
|
||||
document.cookie = escape(name) + "=" + escape(value) + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";
|
||||
};
|
||||
|
||||
cookieService.putSession = function(name, value) {
|
||||
$cookies.put(name, value);
|
||||
};
|
||||
|
||||
cookieService.clear = function(name) {
|
||||
$cookies.remove(name);
|
||||
};
|
||||
|
||||
cookieService.get = function(name) {
|
||||
return $cookies.get(name);
|
||||
};
|
||||
|
||||
return cookieService;
|
||||
}]);
|
60
config_app/js/services/document-visibility-service.js
Normal file
60
config_app/js/services/document-visibility-service.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/**
|
||||
* Helper service which fires off events when the document's visibility changes, as well as allowing
|
||||
* other Angular code to query the state of the document's visibility directly.
|
||||
*/
|
||||
angular.module('quay-config').constant('CORE_EVENT', {
|
||||
DOC_VISIBILITY_CHANGE: 'core.event.doc_visibility_change'
|
||||
});
|
||||
|
||||
angular.module('quay-config').factory('DocumentVisibilityService', ['$rootScope', '$document', 'CORE_EVENT',
|
||||
function($rootScope, $document, CORE_EVENT) {
|
||||
var document = $document[0],
|
||||
features,
|
||||
detectedFeature;
|
||||
|
||||
function broadcastChangeEvent() {
|
||||
$rootScope.$broadcast(CORE_EVENT.DOC_VISIBILITY_CHANGE,
|
||||
document[detectedFeature.propertyName]);
|
||||
}
|
||||
|
||||
features = {
|
||||
standard: {
|
||||
eventName: 'visibilitychange',
|
||||
propertyName: 'hidden'
|
||||
},
|
||||
moz: {
|
||||
eventName: 'mozvisibilitychange',
|
||||
propertyName: 'mozHidden'
|
||||
},
|
||||
ms: {
|
||||
eventName: 'msvisibilitychange',
|
||||
propertyName: 'msHidden'
|
||||
},
|
||||
webkit: {
|
||||
eventName: 'webkitvisibilitychange',
|
||||
propertyName: 'webkitHidden'
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(features).some(function(feature) {
|
||||
if (document[features[feature].propertyName] !== undefined) {
|
||||
detectedFeature = features[feature];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (detectedFeature) {
|
||||
$document.on(detectedFeature.eventName, broadcastChangeEvent);
|
||||
}
|
||||
|
||||
return {
|
||||
/**
|
||||
* Is the window currently hidden or not.
|
||||
*/
|
||||
isHidden: function() {
|
||||
if (detectedFeature) {
|
||||
return document[detectedFeature.propertyName];
|
||||
}
|
||||
}
|
||||
};
|
||||
}]);
|
91
config_app/js/services/features-config.js
Normal file
91
config_app/js/services/features-config.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Feature flags.
|
||||
*/
|
||||
angular.module('quay-config').factory('Features', [function() {
|
||||
if (!window.__features) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var features = window.__features;
|
||||
features.getFeature = function(name, opt_defaultValue) {
|
||||
var value = features[name];
|
||||
if (value == null) {
|
||||
return opt_defaultValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
features.hasFeature = function(name) {
|
||||
return !!features.getFeature(name);
|
||||
};
|
||||
|
||||
features.matchesFeatures = function(list) {
|
||||
for (var i = 0; i < list.length; ++i) {
|
||||
var value = features.getFeature(list[i]);
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return features;
|
||||
}]);
|
||||
|
||||
/**
|
||||
* Application configuration.
|
||||
*/
|
||||
angular.module('quay-config').factory('Config', ['Features', function(Features) {
|
||||
if (!window.__config) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var config = window.__config;
|
||||
config.getDomain = function() {
|
||||
return config['SERVER_HOSTNAME'];
|
||||
};
|
||||
|
||||
config.getHost = function(opt_auth) {
|
||||
var auth = opt_auth;
|
||||
if (auth) {
|
||||
auth = auth + '@';
|
||||
}
|
||||
|
||||
return config['PREFERRED_URL_SCHEME'] + '://' + auth + config['SERVER_HOSTNAME'];
|
||||
};
|
||||
|
||||
config.getHttp = function() {
|
||||
return config['PREFERRED_URL_SCHEME'];
|
||||
};
|
||||
|
||||
config.getUrl = function(opt_path) {
|
||||
var path = opt_path || '';
|
||||
return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path;
|
||||
};
|
||||
|
||||
config.getValue = function(name, opt_defaultValue) {
|
||||
var value = config[name];
|
||||
if (value == null) {
|
||||
return opt_defaultValue;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
config.getEnterpriseLogo = function(opt_defaultValue) {
|
||||
if (!config.ENTERPRISE_LOGO_URL) {
|
||||
if (opt_defaultValue) {
|
||||
return opt_defaultValue;
|
||||
}
|
||||
|
||||
if (Features.BILLING) {
|
||||
return '/static/img/quay-horizontal-color.svg';
|
||||
} else {
|
||||
return '/static/img/QuayEnterprise_horizontal_color.svg';
|
||||
}
|
||||
}
|
||||
|
||||
return config.ENTERPRISE_LOGO_URL;
|
||||
};
|
||||
|
||||
return config;
|
||||
}]);
|
15
config_app/js/services/services.types.ts
Normal file
15
config_app/js/services/services.types.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export interface AngularPollChannel {
|
||||
create: PollConstructor
|
||||
}
|
||||
|
||||
type PollConstructor = (scope: MockAngularScope, requester: ShouldContinueCallback, opt_sleeptime?: number) => PollHandle;
|
||||
type MockAngularScope = {
|
||||
'$on': Function
|
||||
};
|
||||
type ShouldContinueCallback = (boolean) => void;
|
||||
|
||||
export interface PollHandle {
|
||||
start(opt_skipFirstCall?: boolean): void,
|
||||
stop(): void,
|
||||
setSleepTime(sleepTime: number): void,
|
||||
}
|
177
config_app/js/services/user-service.js
Normal file
177
config_app/js/services/user-service.js
Normal file
|
@ -0,0 +1,177 @@
|
|||
import * as Raven from 'raven-js';
|
||||
|
||||
|
||||
/**
|
||||
* Service which monitors the current user session and provides methods for returning information
|
||||
* about the user.
|
||||
*/
|
||||
angular.module('quay-config')
|
||||
.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config', '$location', '$timeout',
|
||||
|
||||
function(ApiService, CookieService, $rootScope, Config, $location, $timeout) {
|
||||
var userResponse = {
|
||||
verified: false,
|
||||
anonymous: true,
|
||||
username: null,
|
||||
email: null,
|
||||
organizations: [],
|
||||
logins: [],
|
||||
beforeload: true
|
||||
};
|
||||
|
||||
var userService = {};
|
||||
|
||||
userService.hasEverLoggedIn = function() {
|
||||
return CookieService.get('quay.loggedin') == 'true';
|
||||
};
|
||||
|
||||
userService.updateUserIn = function(scope, opt_callback) {
|
||||
scope.$watch(function () { return userService.currentUser(); }, function (currentUser) {
|
||||
if (currentUser) {
|
||||
$timeout(function(){
|
||||
scope.user = currentUser;
|
||||
if (opt_callback) {
|
||||
opt_callback(currentUser);
|
||||
}
|
||||
}, 0, false);
|
||||
};
|
||||
}, true);
|
||||
};
|
||||
|
||||
userService.load = function(opt_callback) {
|
||||
var handleUserResponse = function(loadedUser) {
|
||||
userResponse = loadedUser;
|
||||
|
||||
if (!userResponse.anonymous) {
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
try {
|
||||
mixpanel.identify(userResponse.username);
|
||||
mixpanel.people.set({
|
||||
'$email': userResponse.email,
|
||||
'$username': userResponse.username,
|
||||
'verified': userResponse.verified
|
||||
});
|
||||
mixpanel.people.set_once({
|
||||
'$created': new Date()
|
||||
})
|
||||
} catch (e) {
|
||||
window.console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.MARKETO_MUNCHKIN_ID && userResponse['marketo_user_hash']) {
|
||||
var associateLeadBody = {'Email': userResponse.email};
|
||||
if (window.Munchkin !== undefined) {
|
||||
try {
|
||||
Munchkin.munchkinFunction(
|
||||
'associateLead',
|
||||
associateLeadBody,
|
||||
userResponse['marketo_user_hash']
|
||||
);
|
||||
} catch (e) {
|
||||
}
|
||||
} else {
|
||||
window.__quay_munchkin_queue.push([
|
||||
'associateLead',
|
||||
associateLeadBody,
|
||||
userResponse['marketo_user_hash']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (window.Raven !== undefined) {
|
||||
try {
|
||||
Raven.setUser({
|
||||
email: userResponse.email,
|
||||
id: userResponse.username
|
||||
});
|
||||
} catch (e) {
|
||||
window.console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
CookieService.putPermanent('quay.loggedin', 'true');
|
||||
} else {
|
||||
if (window.Raven !== undefined) {
|
||||
Raven.setUser();
|
||||
}
|
||||
}
|
||||
|
||||
// If the loaded user has a prompt, redirect them to the update page.
|
||||
if (loadedUser.prompts && loadedUser.prompts.length) {
|
||||
$location.path('/updateuser');
|
||||
return;
|
||||
}
|
||||
|
||||
if (opt_callback) {
|
||||
opt_callback(loadedUser);
|
||||
}
|
||||
};
|
||||
|
||||
ApiService.getLoggedInUser().then(function(loadedUser) {
|
||||
handleUserResponse(loadedUser);
|
||||
}, function() {
|
||||
handleUserResponse({'anonymous': true});
|
||||
});
|
||||
};
|
||||
|
||||
userService.isOrganization = function(name) {
|
||||
return !!userService.getOrganization(name);
|
||||
};
|
||||
|
||||
userService.getOrganization = function(name) {
|
||||
if (!userResponse || !userResponse.organizations) { return null; }
|
||||
for (var i = 0; i < userResponse.organizations.length; ++i) {
|
||||
var org = userResponse.organizations[i];
|
||||
if (org.name == name) {
|
||||
return org;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
userService.isNamespaceAdmin = function(namespace) {
|
||||
if (namespace == userResponse.username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var org = userService.getOrganization(namespace);
|
||||
if (!org) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return org.is_org_admin;
|
||||
};
|
||||
|
||||
userService.isKnownNamespace = function(namespace) {
|
||||
if (namespace == userResponse.username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var org = userService.getOrganization(namespace);
|
||||
return !!org;
|
||||
};
|
||||
|
||||
userService.getNamespace = function(namespace) {
|
||||
var org = userService.getOrganization(namespace);
|
||||
if (org) {
|
||||
return org;
|
||||
}
|
||||
|
||||
if (namespace == userResponse.username) {
|
||||
return userResponse;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
userService.currentUser = function() {
|
||||
return userResponse;
|
||||
};
|
||||
|
||||
// Update the user in the root scope.
|
||||
userService.updateUserIn($rootScope);
|
||||
|
||||
return userService;
|
||||
}]);
|
83
config_app/js/services/util-service.js
Normal file
83
config_app/js/services/util-service.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* Service which exposes various utility methods.
|
||||
*/
|
||||
angular.module('quay-config').factory('UtilService', ['$sanitize',
|
||||
function($sanitize) {
|
||||
var utilService = {};
|
||||
|
||||
var adBlockEnabled = null;
|
||||
|
||||
utilService.isAdBlockEnabled = function(callback) {
|
||||
if (adBlockEnabled !== null) {
|
||||
callback(adBlockEnabled);
|
||||
return;
|
||||
}
|
||||
|
||||
if(typeof blockAdBlock === 'undefined') {
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
|
||||
var bab = new BlockAdBlock({
|
||||
checkOnLoad: false,
|
||||
resetOnEnd: true
|
||||
});
|
||||
|
||||
bab.onDetected(function() { adBlockEnabled = true; callback(true); });
|
||||
bab.onNotDetected(function() { adBlockEnabled = false; callback(false); });
|
||||
bab.check();
|
||||
};
|
||||
|
||||
utilService.isEmailAddress = function(val) {
|
||||
var emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
|
||||
return emailRegex.test(val);
|
||||
};
|
||||
|
||||
utilService.escapeHtmlString = function(text) {
|
||||
var textStr = (text || '').toString();
|
||||
var adjusted = textStr.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
return adjusted;
|
||||
};
|
||||
|
||||
utilService.stringToHTML = function(text) {
|
||||
text = utilService.escapeHtmlString(text);
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
return text;
|
||||
};
|
||||
|
||||
utilService.getRestUrl = function(args) {
|
||||
var url = '';
|
||||
for (var i = 0; i < arguments.length; ++i) {
|
||||
if (i > 0) {
|
||||
url += '/';
|
||||
}
|
||||
url += encodeURI(arguments[i])
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
utilService.textToSafeHtml = function(text) {
|
||||
return $sanitize(utilService.escapeHtmlString(text));
|
||||
};
|
||||
|
||||
return utilService;
|
||||
}])
|
||||
.factory('CoreDialog', [() => {
|
||||
var service = {};
|
||||
service['fatal'] = function(title, message) {
|
||||
bootbox.dialog({
|
||||
"title": title,
|
||||
"message": "<div class='alert-icon-container-container'><div class='alert-icon-container'><div class='alert-icon'></div></div></div>" + message,
|
||||
"buttons": {},
|
||||
"className": "co-dialog fatal-error",
|
||||
"closeButton": false
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
319
config_app/js/setup/setup.component.js
Normal file
319
config_app/js/setup/setup.component.js
Normal file
|
@ -0,0 +1,319 @@
|
|||
import * as URI from 'urijs';
|
||||
const templateUrl = require('./setup.html');
|
||||
|
||||
(function() {
|
||||
/**
|
||||
* The Setup page provides a nice GUI walkthrough experience for setting up Red Hat Quay.
|
||||
*/
|
||||
|
||||
angular.module('quay-config').directive('setup', () => {
|
||||
const directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
templateUrl,
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'isActive': '=isActive',
|
||||
'configurationSaved': '&configurationSaved',
|
||||
'setupCompleted': '&setupCompleted',
|
||||
},
|
||||
controller: SetupCtrl,
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
})
|
||||
|
||||
function SetupCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, CoreDialog) {
|
||||
// if (!Features.SUPER_USERS) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9_\.\-]+(:[0-9]+)?$';
|
||||
|
||||
$scope.validateHostname = function(hostname) {
|
||||
if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
|
||||
return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Note: The values of the enumeration are important for isStepFamily. For example,
|
||||
// *all* states under the "configuring db" family must start with "config-db".
|
||||
$scope.States = {
|
||||
// Loading the state of the product.
|
||||
'LOADING': 'loading',
|
||||
|
||||
// The configuration directory is missing.
|
||||
'MISSING_CONFIG_DIR': 'missing-config-dir',
|
||||
|
||||
// The config.yaml exists but it is invalid.
|
||||
'INVALID_CONFIG': 'config-invalid',
|
||||
|
||||
// DB is being configured.
|
||||
'CONFIG_DB': 'config-db',
|
||||
|
||||
// DB information is being validated.
|
||||
'VALIDATING_DB': 'config-db-validating',
|
||||
|
||||
// DB information is being saved to the config.
|
||||
'SAVING_DB': 'config-db-saving',
|
||||
|
||||
// A validation error occurred with the database.
|
||||
'DB_ERROR': 'config-db-error',
|
||||
|
||||
// Database is being setup.
|
||||
'DB_SETUP': 'setup-db',
|
||||
|
||||
// An error occurred when setting up the database.
|
||||
'DB_SETUP_ERROR': 'setup-db-error',
|
||||
|
||||
// A superuser is being configured.
|
||||
'CREATE_SUPERUSER': 'create-superuser',
|
||||
|
||||
// The superuser is being created.
|
||||
'CREATING_SUPERUSER': 'create-superuser-creating',
|
||||
|
||||
// An error occurred when setting up the superuser.
|
||||
'SUPERUSER_ERROR': 'create-superuser-error',
|
||||
|
||||
// The superuser was created successfully.
|
||||
'SUPERUSER_CREATED': 'create-superuser-created',
|
||||
|
||||
// General configuration is being setup.
|
||||
'CONFIG': 'config',
|
||||
|
||||
// The configuration is fully valid.
|
||||
'VALID_CONFIG': 'valid-config',
|
||||
|
||||
// The product is ready for use.
|
||||
'READY': 'ready'
|
||||
}
|
||||
|
||||
$scope.csrf_token = window.__token;
|
||||
$scope.currentStep = $scope.States.LOADING;
|
||||
$scope.errors = {};
|
||||
$scope.stepProgress = [];
|
||||
$scope.hasSSL = false;
|
||||
$scope.hostname = null;
|
||||
$scope.currentConfig = null;
|
||||
|
||||
$scope.currentState = {
|
||||
'hasDatabaseSSLCert': false
|
||||
};
|
||||
|
||||
$scope.$watch('currentStep', function(currentStep) {
|
||||
$scope.stepProgress = $scope.getProgress(currentStep);
|
||||
|
||||
switch (currentStep) {
|
||||
case $scope.States.CONFIG:
|
||||
$('#setupModal').modal('hide');
|
||||
break;
|
||||
|
||||
case $scope.States.MISSING_CONFIG_DIR:
|
||||
$scope.showMissingConfigDialog();
|
||||
break;
|
||||
|
||||
case $scope.States.INVALID_CONFIG:
|
||||
$scope.showInvalidConfigDialog();
|
||||
break;
|
||||
|
||||
case $scope.States.DB_SETUP:
|
||||
$scope.performDatabaseSetup();
|
||||
// Fall-through.
|
||||
|
||||
case $scope.States.CREATE_SUPERUSER:
|
||||
case $scope.States.CONFIG_DB:
|
||||
case $scope.States.VALID_CONFIG:
|
||||
case $scope.States.READY:
|
||||
$('#setupModal').modal({
|
||||
keyboard: false,
|
||||
backdrop: 'static'
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.restartContainer = function(state) {
|
||||
$scope.currentStep = state;
|
||||
ContainerService.restartContainer(function() {
|
||||
$scope.checkStatus()
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showSuperuserPanel = function() {
|
||||
$('#setupModal').modal('hide');
|
||||
var prefix = $scope.hasSSL ? 'https' : 'http';
|
||||
var hostname = $scope.hostname;
|
||||
if (!hostname) {
|
||||
hostname = document.location.hostname;
|
||||
if (document.location.port) {
|
||||
hostname = hostname + ':' + document.location.port;
|
||||
}
|
||||
}
|
||||
|
||||
window.location = prefix + '://' + hostname + '/superuser';
|
||||
};
|
||||
|
||||
$scope.configurationSaved = function(config) {
|
||||
$scope.hasSSL = config['PREFERRED_URL_SCHEME'] == 'https';
|
||||
$scope.hostname = config['SERVER_HOSTNAME'];
|
||||
$scope.currentConfig = config;
|
||||
|
||||
$scope.currentStep = $scope.States.VALID_CONFIG;
|
||||
};
|
||||
|
||||
$scope.getProgress = function(step) {
|
||||
var isStep = $scope.isStep;
|
||||
var isStepFamily = $scope.isStepFamily;
|
||||
var States = $scope.States;
|
||||
|
||||
return [
|
||||
isStepFamily(step, States.CONFIG_DB),
|
||||
isStepFamily(step, States.DB_SETUP),
|
||||
isStepFamily(step, States.CREATE_SUPERUSER),
|
||||
isStep(step, States.CONFIG),
|
||||
isStep(step, States.VALID_CONFIG),
|
||||
isStep(step, States.READY)
|
||||
];
|
||||
};
|
||||
|
||||
$scope.isStepFamily = function(step, family) {
|
||||
if (!step) { return false; }
|
||||
return step.indexOf(family) == 0;
|
||||
};
|
||||
|
||||
$scope.isStep = function(step) {
|
||||
for (var i = 1; i < arguments.length; ++i) {
|
||||
if (arguments[i] == step) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.beginSetup = function() {
|
||||
$scope.currentStep = $scope.States.CONFIG_DB;
|
||||
};
|
||||
|
||||
$scope.showInvalidConfigDialog = function() {
|
||||
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
|
||||
var title = "Invalid configuration file";
|
||||
CoreDialog.fatal(title, message);
|
||||
};
|
||||
|
||||
|
||||
$scope.showMissingConfigDialog = function() {
|
||||
var message = "A volume should be mounted into the container at <code>/conf/stack</code>: " +
|
||||
"<br><br><pre>docker run -v /path/to/config:/conf/stack</pre>" +
|
||||
"<br>Once fixed, restart the container. For more information, " +
|
||||
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
|
||||
"Read the Setup Guide</a>"
|
||||
|
||||
var title = "Missing configuration volume";
|
||||
CoreDialog.fatal(title, message);
|
||||
};
|
||||
|
||||
$scope.parseDbUri = function(value) {
|
||||
if (!value) { return null; }
|
||||
|
||||
// Format: mysql+pymysql://<username>:<url escaped password>@<hostname>/<database_name>
|
||||
var uri = URI(value);
|
||||
return {
|
||||
'kind': uri.protocol(),
|
||||
'username': uri.username(),
|
||||
'password': uri.password(),
|
||||
'server': uri.host(),
|
||||
'database': uri.path() ? uri.path().substr(1) : ''
|
||||
};
|
||||
};
|
||||
|
||||
$scope.serializeDbUri = function(fields) {
|
||||
if (!fields['server']) { return ''; }
|
||||
if (!fields['database']) { return ''; }
|
||||
|
||||
var uri = URI();
|
||||
try {
|
||||
uri = uri && uri.host(fields['server']);
|
||||
uri = uri && uri.protocol(fields['kind']);
|
||||
uri = uri && uri.username(fields['username']);
|
||||
uri = uri && uri.password(fields['password']);
|
||||
uri = uri && uri.path('/' + (fields['database'] || ''));
|
||||
uri = uri && uri.toString();
|
||||
} catch (ex) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return uri;
|
||||
};
|
||||
|
||||
$scope.createSuperUser = function() {
|
||||
$scope.currentStep = $scope.States.CREATING_SUPERUSER;
|
||||
ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) {
|
||||
$scope.checkStatus();
|
||||
}, function(resp) {
|
||||
$scope.currentStep = $scope.States.SUPERUSER_ERROR;
|
||||
$scope.errors.SuperuserCreationError = ApiService.getErrorMessage(resp, 'Could not create superuser');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.performDatabaseSetup = function() {
|
||||
$scope.currentStep = $scope.States.DB_SETUP;
|
||||
ApiService.scSetupDatabase(null, null).then(function(resp) {
|
||||
if (resp['error']) {
|
||||
$scope.currentStep = $scope.States.DB_SETUP_ERROR;
|
||||
$scope.errors.DatabaseSetupError = resp['error'];
|
||||
} else {
|
||||
$scope.currentStep = $scope.States.CREATE_SUPERUSER;
|
||||
}
|
||||
}, ApiService.errorDisplay('Could not setup database. Please report this to support.'))
|
||||
};
|
||||
|
||||
$scope.validateDatabase = function() {
|
||||
$scope.currentStep = $scope.States.VALIDATING_DB;
|
||||
$scope.databaseInvalid = null;
|
||||
|
||||
var data = {
|
||||
'config': {
|
||||
'DB_URI': $scope.databaseUri
|
||||
},
|
||||
};
|
||||
|
||||
if ($scope.currentState.hasDatabaseSSLCert) {
|
||||
data['config']['DB_CONNECTION_ARGS'] = {
|
||||
'ssl': {
|
||||
'ca': 'conf/stack/database.pem'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var params = {
|
||||
'service': 'database'
|
||||
};
|
||||
|
||||
ApiService.scValidateConfig(data, params).then(function(resp) {
|
||||
var status = resp.status;
|
||||
|
||||
if (status) {
|
||||
$scope.currentStep = $scope.States.SAVING_DB;
|
||||
ApiService.scUpdateConfig(data, null).then(function(resp) {
|
||||
$scope.checkStatus();
|
||||
}, ApiService.errorDisplay('Cannot update config. Please report this to support'));
|
||||
} else {
|
||||
$scope.currentStep = $scope.States.DB_ERROR;
|
||||
$scope.errors.DatabaseValidationError = resp.reason;
|
||||
}
|
||||
}, ApiService.errorDisplay('Cannot validate database. Please report this to support'));
|
||||
};
|
||||
|
||||
$scope.checkStatus = function() {
|
||||
ContainerService.checkStatus(function(resp) {
|
||||
$scope.currentStep = resp['status'];
|
||||
}, $scope.currentConfig);
|
||||
};
|
||||
|
||||
// Load the initial status.
|
||||
$scope.checkStatus();
|
||||
};
|
||||
})();
|
307
config_app/js/setup/setup.html
Normal file
307
config_app/js/setup/setup.html
Normal file
|
@ -0,0 +1,307 @@
|
|||
<div id="padding-container">
|
||||
<div>
|
||||
<div class="cor-loader" ng-show="currentStep == States.LOADING"></div>
|
||||
<div class="page-content" ng-show="currentStep == States.CONFIG">
|
||||
<div class="cor-title">
|
||||
<span class="cor-title-link"></span>
|
||||
<span class="cor-title-content">Red Hat Quay Setup</span>
|
||||
</div>
|
||||
|
||||
<div class="co-main-content-panel" style="padding: 20px;">
|
||||
<div class="co-alert alert alert-info">
|
||||
<span class="cor-step-bar" progress="stepProgress">
|
||||
<span class="cor-step" title="Configure Database" text="1"></span>
|
||||
<span class="cor-step" title="Setup Database" icon="database"></span>
|
||||
<span class="cor-step" title="Create Superuser" text="2"></span>
|
||||
<span class="cor-step" title="Configure Registry" text="3"></span>
|
||||
<span class="cor-step" title="Validate Configuration" text="4"></span>
|
||||
<span class="cor-step" title="Setup Complete" icon="download"></span>
|
||||
</span>
|
||||
|
||||
<div><strong>Almost done!</strong></div>
|
||||
<div>Configure your Redis database and other settings below</div>
|
||||
</div>
|
||||
|
||||
<div class="config-setup-tool" is-active="isStep(currentStep, States.CONFIG)"
|
||||
configuration-saved="configurationSaved(config)"
|
||||
setup-completed="setupCompleted()"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="co-dialog modal fade initial-setup-modal" id="setupModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<span class="cor-step-bar" progress="stepProgress">
|
||||
<span class="cor-step" title="Configure Database" text="1"></span>
|
||||
<span class="cor-step" title="Setup Database" icon="database"></span>
|
||||
<span class="cor-step" title="Create Superuser" text="2"></span>
|
||||
<span class="cor-step" title="Configure Registry" text="3"></span>
|
||||
<span class="cor-step" title="Validate Configuration" text="4"></span>
|
||||
<span class="cor-step" title="Setup Complete" icon="download"></span>
|
||||
</span>
|
||||
<h4 class="modal-title">Setup</h4>
|
||||
</div>
|
||||
|
||||
<form id="superuserForm" name="superuserForm" ng-submit="createSuperUser()">
|
||||
<!-- Content: CREATE_SUPERUSER or SUPERUSER_ERROR or CREATING_SUPERUSER -->
|
||||
<div class="modal-body config-setup-tool-element" style="padding: 20px"
|
||||
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR, States.CREATING_SUPERUSER)">
|
||||
<p>A superuser is the main administrator of your <span class="registry-name" is-short="true"></span>. Only superusers can edit configuration settings.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input class="form-control" type="text" ng-model="superUser.username"
|
||||
ng-pattern="/^[a-z0-9_]{4,30}$/" required>
|
||||
<div class="help-text">Minimum 4 characters in length</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Email address</label>
|
||||
<input class="form-control" type="email" ng-model="superUser.email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Password</label>
|
||||
<input class="form-control" type="password" ng-model="superUser.password"
|
||||
ng-pattern="/^[^\s]+$/"
|
||||
ng-minlength="8" required>
|
||||
<div class="help-text">Minimum 8 characters in length</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Repeat Password</label>
|
||||
<input class="form-control" type="password" ng-model="superUser.repeatPassword"
|
||||
match="superUser.password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer: CREATE_SUPERUSER or SUPERUSER_ERROR -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.CREATE_SUPERUSER, States.SUPERUSER_ERROR)">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="!superuserForm.$valid">
|
||||
Create Super User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Content: DB_RESTARTING or CONFIG_RESTARTING -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
|
||||
<h4 style="margin-bottom: 20px;">
|
||||
<i class="fa fa-lg fa-sync" style="margin-right: 10px;"></i>
|
||||
<span class="registry-name"></span> is currently being restarted
|
||||
</h4>
|
||||
This can take several minutes. If the container does not restart on its own,
|
||||
please re-execute the <code>docker run</code> command.
|
||||
</div>
|
||||
|
||||
<!-- Content: READY -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.READY)">
|
||||
<h4>Installation and setup of <span class="registry-name"></span> is complete</h4>
|
||||
You can now invite users to join, create organizations and start pushing and pulling
|
||||
repositories.
|
||||
|
||||
<strong ng-if="hasSSL" style="margin-top: 20px;">
|
||||
Note: SSL is enabled. Please make sure to visit with
|
||||
an <u>https</u> prefix
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<!-- Content: VALID_CONFIG -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.VALID_CONFIG)">
|
||||
<h4>All configuration has been validated and saved</h4>
|
||||
The container must be restarted to apply the configuration changes.
|
||||
</div>
|
||||
|
||||
<!-- Content: DB_SETUP_SUCCESS -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
|
||||
<h4>The database has been setup and is ready</h4>
|
||||
The container must be restarted to apply the configuration changes.
|
||||
</div>
|
||||
|
||||
<!-- Content: DB_SETUP or DB_SETUP_ERROR -->
|
||||
<div class="modal-body" style="padding: 20px;"
|
||||
ng-show="isStep(currentStep, States.DB_SETUP, States.DB_SETUP_ERROR)">
|
||||
<h4>
|
||||
<i class="fa fa-lg fa-database" style="margin-right: 10px;"></i>
|
||||
<span class="registry-name"></span> is currently setting up its database
|
||||
schema
|
||||
</h4>
|
||||
This can take several minutes.
|
||||
</div>
|
||||
|
||||
<!-- Content: CONFIG_DB or DB_ERROR or VALIDATING_DB or SAVING_DB -->
|
||||
<div class="modal-body validate-database config-setup-tool-element"
|
||||
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR, States.VALIDATING_DB, States.SAVING_DB)">
|
||||
<p>
|
||||
Please enter the connection details for your <strong>empty</strong> database. The schema will be created in the following step.</p>
|
||||
</p>
|
||||
|
||||
<div class="config-parsed-field" binding="databaseUri"
|
||||
parser="parseDbUri(value)"
|
||||
serializer="serializeDbUri(fields)">
|
||||
<table class="config-table">
|
||||
<tr>
|
||||
<td class="non-input">Database Type:</td>
|
||||
<td>
|
||||
<select ng-model="fields.kind">
|
||||
<option value="mysql+pymysql">MySQL</option>
|
||||
<option value="postgresql">Postgres</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>Database Server:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="fields.server"
|
||||
placeholder="dbserverhost"
|
||||
pattern="{{ HOSTNAME_REGEX }}"
|
||||
validator="validateHostname(value)">></span>
|
||||
<div class="help-text">
|
||||
The server (and optionally, custom port) where the database lives
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>Username:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="fields.username"
|
||||
placeholder="someuser"></span>
|
||||
<div class="help-text">This user must have <strong>full access</strong> to the database</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>Password:</td>
|
||||
<td>
|
||||
<input class="form-control" type="password" ng-model="fields.password"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>Database Name:</td>
|
||||
<td>
|
||||
<span class="config-string-field" binding="fields.database"
|
||||
placeholder="registry-database"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="fields.kind">
|
||||
<td>SSL Certificate:</td>
|
||||
<td>
|
||||
<span class="config-file-field" filename="database.pem"
|
||||
skip-check-file="true" has-file="currentState.hasDatabaseSSLCert"></span>
|
||||
<div class="help-text">Optional SSL certicate (in PEM format) to use to connect to the database</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer: CREATING_SUPERUSER -->
|
||||
<div class="modal-footer working" ng-show="isStep(currentStep, States.CREATING_SUPERUSER)">
|
||||
<span class="cor-loader-inline"></span> Creating superuser...
|
||||
</div>
|
||||
|
||||
<!-- Footer: SUPERUSER_ERROR -->
|
||||
<div class="modal-footer alert alert-danger"
|
||||
ng-show="isStep(currentStep, States.SUPERUSER_ERROR)">
|
||||
{{ errors.SuperuserCreationError }}
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_SETUP_ERROR -->
|
||||
<div class="modal-footer alert alert-danger"
|
||||
ng-show="isStep(currentStep, States.DB_SETUP_ERROR)">
|
||||
Database Setup Failed: {{ errors.DatabaseSetupError }}
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_ERROR -->
|
||||
<div class="modal-footer alert alert-danger" ng-show="isStep(currentStep, States.DB_ERROR)">
|
||||
Database Validation Issue: {{ errors.DatabaseValidationError }}
|
||||
</div>
|
||||
|
||||
<!-- Footer: CONFIG_DB or DB_ERROR -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.CONFIG_DB, States.DB_ERROR)">
|
||||
<span class="left-align" ng-show="isStep(currentStep, States.DB_ERROR)">
|
||||
<i class="fa fa-warning"></i>
|
||||
Problem Detected
|
||||
</span>
|
||||
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-disabled="!databaseUri"
|
||||
ng-click="validateDatabase()">
|
||||
Validate Database Settings
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Footer: READY -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.READY)">
|
||||
<span class="left-align">
|
||||
<i class="fa fa-check"></i>
|
||||
Installation Complete!
|
||||
</span>
|
||||
|
||||
<a ng-click="showSuperuserPanel()" class="btn btn-primary">
|
||||
View Superuser Panel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Footer: VALID_CONFIG -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.VALID_CONFIG)">
|
||||
<span class="left-align">
|
||||
<i class="fa fa-check"></i>
|
||||
Configuration Validated and Saved
|
||||
</span>
|
||||
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-click="restartContainer(States.CONFIG_RESTARTING)">
|
||||
Restart Container
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_SETUP_SUCCESS -->
|
||||
<div class="modal-footer"
|
||||
ng-show="isStep(currentStep, States.DB_SETUP_SUCCESS)">
|
||||
<span class="left-align">
|
||||
<i class="fa fa-check"></i>
|
||||
Database Setup and Ready
|
||||
</span>
|
||||
|
||||
<button type="submit" class="btn btn-primary"
|
||||
ng-click="restartContainer(States.DB_RESTARTING)">
|
||||
Restart Container
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_SETUP -->
|
||||
<div class="modal-footer working" ng-show="isStep(currentStep, States.DB_SETUP)">
|
||||
<span class="cor-loader-inline"></span> Setting up database...
|
||||
</div>
|
||||
|
||||
<!-- Footer: SAVING_DB -->
|
||||
<div class="modal-footer working" ng-show="isStep(currentStep, States.SAVING_DB)">
|
||||
<span class="cor-loader-inline"></span> Saving database configuration...
|
||||
</div>
|
||||
|
||||
<!-- Footer: VALIDATING_DB -->
|
||||
<div class="modal-footer working" ng-show="isStep(currentStep, States.VALIDATING_DB)">
|
||||
<span class="cor-loader-inline"></span> Testing database settings...
|
||||
</div>
|
||||
|
||||
<!-- Footer: DB_RESTARTING or CONFIG_RESTARTING-->
|
||||
<div class="modal-footer working"
|
||||
ng-show="isStep(currentStep, States.DB_RESTARTING, States.CONFIG_RESTARTING)">
|
||||
<span class="cor-loader-inline"></span> Waiting for container to restart...
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</div>
|
Reference in a new issue