app.portBindings and newManifest.tcpPorts may be null
This commit is contained in:
161
webadmin/src/views/account.html
Normal file
161
webadmin/src/views/account.html
Normal file
@@ -0,0 +1,161 @@
|
||||
<!-- Modal change password -->
|
||||
<div class="modal fade" id="passwordChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change Your Password</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="passwordchange_form" class="form-signin" role="form" novalidate ng-submit="doChangePassword(passwordchange_form)" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (passwordchange_form.password.$dirty && passwordchange_form.password.$invalid)}">
|
||||
<label class="control-label" for="inputPasswordChangePassword">Current Password</label>
|
||||
<div class="control-label" ng-show="(!passwordchange_form.password.$dirty && passwordchange.error.password) || (passwordchange_form.password.$dirty && passwordchange_form.password.$invalid)">
|
||||
<small ng-show="passwordchange_form.password.$error.required">A password is required</small>
|
||||
<small ng-show="passwordchange_form.password.$error.minlength">The password is too short</small>
|
||||
<small ng-show="passwordchange_form.password.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="passwordchange_form.password.$dirty && passwordchange.error.password">Invalid pasword</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.password" id="inputPasswordChangePassword" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (passwordchange_form.newPassword.$dirty && passwordchange_form.newPassword.$invalid)}">
|
||||
<label class="control-label" for="inputnewpassword">New Password</label>
|
||||
<div class="control-label" ng-show="(!passwordchange_form.newPassword.$dirty && passwordchange.error.newPassword) || (passwordchange_form.newPassword.$dirty && passwordchange_form.newPassword.$invalid)">
|
||||
<small ng-show="passwordchange_form.newPassword.$error.required">A password is required</small>
|
||||
<small ng-show="passwordchange_form.newPassword.$error.minlength">The password is too short</small>
|
||||
<small ng-show="passwordchange_form.newPassword.$error.maxlength">The password is too long</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPassword" id="inputnewpassword" name="newPassword" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (passwordchange_form.newPasswordRepeat.$dirty && passwordchange_form.newPasswordRepeat.$invalid) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat) }">
|
||||
<label class="control-label" for="inputnewpasswordrepeat">Repeat New Password</label>
|
||||
<div class="control-label" ng-show="(!passwordchange_form.newPasswordRepeat.$dirty && passwordchange.error.newPasswordRepeat) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange_form.newPasswordRepeat.$invalid) || (passwordchange_form.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat)">
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$error.required">A password is required</small>
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$error.minlength">The password is too short</small>
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="passwordchange_form.newPasswordRepeat.$dirty && passwordchange.newPassword !== passwordchange.newPasswordRepeat">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordchange.newPasswordRepeat" id="inputnewpasswordrepeat" name="newPasswordRepeat" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="passwordchange_form.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doChangePassword(passwordchange_form)" ng-disabled="passwordchange_form.$invalid || passwordchange.busy"><i class="fa fa-spinner fa-pulse" ng-show="passwordchange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change email -->
|
||||
<div class="modal fade" id="emailChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change Your Email</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="emailchange_form" class="form-signin" role="form" novalidate ng-submit="doChangeEmail(emailchange_form)" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailchange_form.email.$dirty && emailchange_form.email.$invalid) }">
|
||||
<label class="control-label" for="inputEmailChangeEmail">New Email Address</label>
|
||||
<div class="control-label" ng-show="(!emailchange_form.email.$dirty && emailchange.error.email) || (emailchange_form.email.$dirty && emailchange_form.email.$invalid)">
|
||||
<small ng-show="emailchange_form.email.$error.required">A valid email address is required</small>
|
||||
<small ng-show="(emailchange_form.email.$dirty && emailchange_form.email.$invalid) && !emailchange_form.email.$error.required">The Email address is not valid</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="emailchange.email" id="inputEmailChangeEmail" name="email" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (emailchange_form.password.$dirty && emailchange_form.password.$invalid)}">
|
||||
<label class="control-label" for="inputEmailChangePassword">Password</label>
|
||||
<div class="control-label" ng-show="(!emailchange_form.password.$dirty && emailchange.error.password) || (emailchange_form.password.$dirty && emailchange_form.password.$invalid)">
|
||||
<small ng-show="emailchange_form.password.$error.required">A password is required</small>
|
||||
<small ng-show="emailchange_form.password.$error.minlength">The password is too short</small>
|
||||
<small ng-show="emailchange_form.password.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="emailchange_form.password.$dirty && emailchange.error.password">Invalid pasword</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="emailchange.password" id="inputEmailChangePassword" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="emailchange_form.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doChangeEmail(emailchange_form)" ng-disabled="emailchange_form.$invalid || emailchange.busy"><i class="fa fa-spinner fa-pulse" ng-show="emailchange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="text-left">
|
||||
<h1>Account</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<img width="128" height="128" ng-src="{{ user.gravatarHuge }}"/>
|
||||
</div>
|
||||
<div class="col-xs-8 text-medium">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Username</td>
|
||||
<td class="text-right" style="vertical-align: top;">{{ user.username }} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Email</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ user.email }} <a href="" ng-click="showChangeEmail(emailchange_form)"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-right" colspan="2" style="vertical-align: top;">
|
||||
<br/>
|
||||
<button class="btn btn-outline btn-xs btn-danger" ng-click="showChangePassword(passwordchange_form)">Change Password</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="text-left">
|
||||
<h3>Application Access</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- we will always at least have the webadmin token here, so activeClients always will have one entry with at least one token -->
|
||||
<div class="card" ng-repeat="client in activeClients" style="margin-bottom: 15px;" ng-hide="client.tokenCount === 0">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h4 class="text-muted">{{client.name}} on {{client.location}}{{ config.isCustomDomain ? '.' : '-' }}{{config.fqdn}}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
You logged in <b>{{ client.tokenCount }}</b> times to this application.
|
||||
<button class="btn btn-xs btn-danger pull-right" ng-click="removeAccessTokens(client)" ng-disabled="!client.tokenCount || client.busy"><i class="fa fa-spinner fa-pulse" ng-show="client.busy"></i> Remove access</button>
|
||||
<br/>
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse{{client.id}}">Advanced</a>
|
||||
<div id="collapse{{client.id}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<h4 class="text-muted">Credentials</h4>
|
||||
<p>Permissions: <b>{{ client.scope }}</b></p>
|
||||
<p>Client ID: <b>{{ client.id }}</b></p>
|
||||
<p>Client Secret: <b>{{ client.clientSecret }}</b></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
148
webadmin/src/views/account.js
Normal file
148
webadmin/src/views/account.js
Normal file
@@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('AccountController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.activeClients = [];
|
||||
$scope.tokenInUse = null;
|
||||
|
||||
$scope.passwordchange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
password: '',
|
||||
newPassword: '',
|
||||
newPasswordRepeat: ''
|
||||
};
|
||||
|
||||
$scope.emailchange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
email: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
function passwordChangeReset (form) {
|
||||
$scope.passwordchange.error.password = null;
|
||||
$scope.passwordchange.error.newPassword = null;
|
||||
$scope.passwordchange.error.newPasswordRepeat = null;
|
||||
$scope.passwordchange.password = '';
|
||||
$scope.passwordchange.newPassword = '';
|
||||
$scope.passwordchange.newPasswordRepeat = '';
|
||||
|
||||
if (form) {
|
||||
form.$setPristine();
|
||||
form.$setUntouched();
|
||||
}
|
||||
}
|
||||
|
||||
function emailChangeReset (form) {
|
||||
$scope.emailchange.error.email = null;
|
||||
$scope.emailchange.error.password = null;
|
||||
$scope.emailchange.email = '';
|
||||
$scope.emailchange.password = '';
|
||||
|
||||
if (form) {
|
||||
form.$setPristine();
|
||||
form.$setUntouched();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.doChangePassword = function (form) {
|
||||
$scope.passwordchange.error.password = null;
|
||||
$scope.passwordchange.error.newPassword = null;
|
||||
$scope.passwordchange.error.newPasswordRepeat = null;
|
||||
$scope.passwordchange.busy = true;
|
||||
|
||||
Client.changePassword($scope.passwordchange.password, $scope.passwordchange.newPassword, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 403) {
|
||||
$scope.passwordchange.error.password = true;
|
||||
$scope.passwordchange.password = '';
|
||||
$('#inputPasswordChangePassword').focus();
|
||||
} else {
|
||||
console.error('Unable to change password.', error);
|
||||
}
|
||||
} else {
|
||||
passwordChangeReset(form);
|
||||
|
||||
$('#passwordChangeModal').modal('hide');
|
||||
}
|
||||
|
||||
$scope.passwordchange.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doChangeEmail = function (form) {
|
||||
$scope.emailchange.error.email = null;
|
||||
$scope.emailchange.error.password = null;
|
||||
$scope.emailchange.busy = true;
|
||||
|
||||
Client.changeEmail($scope.emailchange.email, $scope.emailchange.password, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 403) {
|
||||
$scope.emailchange.error.password = true;
|
||||
$scope.emailchange.password = '';
|
||||
$('#inputEmailChangePassword').focus();
|
||||
} else {
|
||||
console.error('Unable to change email.', error);
|
||||
}
|
||||
} else {
|
||||
emailChangeReset(form);
|
||||
|
||||
// update user info in the background
|
||||
Client.refreshUserInfo();
|
||||
|
||||
$('#emailChangeModal').modal('hide');
|
||||
}
|
||||
|
||||
$scope.emailchange.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showChangePassword = function (form) {
|
||||
passwordChangeReset(form);
|
||||
|
||||
$('#passwordChangeModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.showChangeEmail = function (form) {
|
||||
emailChangeReset(form);
|
||||
|
||||
$('#emailChangeModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.removeAccessTokens = function (client) {
|
||||
client.busy = true;
|
||||
|
||||
Client.delTokensByClientId(client.id, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
client.busy = false;
|
||||
|
||||
// update the list
|
||||
Client.getOAuthClients(function (error, activeClients) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.activeClients = activeClients;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
$scope.tokenInUse = Client._token;
|
||||
|
||||
Client.getOAuthClients(function (error, activeClients) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.activeClients = activeClients;
|
||||
});
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['passwordChangeModal', 'emailChangeModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
243
webadmin/src/views/apps.html
Normal file
243
webadmin/src/views/apps.html
Normal file
@@ -0,0 +1,243 @@
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length === 0">
|
||||
<div class="col-lg-6 col-lg-offset-3" style="text-align: center;">
|
||||
<br/><br/><br/><br/>
|
||||
<h1><i class="fa fa-cloud-download fa-fw"></i> Your Cloudron does not have any apps installed yet!</h1>
|
||||
<br/></br>
|
||||
<h3>How about installing some? Checkout the <a href="#/appstore">App Store</a></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal configure app -->
|
||||
<div class="modal fade" id="appConfigureModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Configure {{ appConfigure.app.manifest.title }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form class="form-signin" role="form" name="appConfigureForm" ng-submit="doConfigure()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.other">{{ appConfigure.error.other }}</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.location.$dirty && appConfigureForm.location.$invalid) || (!appConfigureForm.location.$dirty && appConfigure.error.location) }">
|
||||
<label class="control-label" for="appConfigureLocationInput">Location {{ appConfigure.error.location }} </label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appConfigure.location" id="appConfigureLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
|
||||
<div class="input-group-addon">
|
||||
{{ !appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-error text-center" ng-show="appConfigure.error.port">{{ appConfigure.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in appConfigure.portBindingsInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appConfigureForm.itemName{{$index}}.$dirty && appConfigure.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="appConfigurePortInput{{env}}"><input type="checkbox" ng-model="appConfigure.portBindingsEnabled[env]"> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
|
||||
<input type="number" class="form-control" ng-model="appConfigure.portBindings[env]" ng-disabled="!appConfigure.portBindingsEnabled[env]" id="appConfigurePortInput{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="accessRestriction">Website Visibility</label>
|
||||
<select class="form-control" id="accessRestriction" ng-model="appConfigure.accessRestriction">
|
||||
<option value="">Visible to all</option>
|
||||
<option value="roleUser">Visible only to Cloudron users</option>
|
||||
</select>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password) }">
|
||||
<label class="control-label" for="appConfigurePasswordInput">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="appConfigure.password" id="appConfigurePasswordInput" name="password" ng-maxlength="512" ng-minlength="5" required>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appConfigureForm.$invalid || busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-click="startApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'stopped' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-play"></i> Start</button>
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-show="appConfigure.app.runState !== 'stopped' && appConfigure.app.runState !== 'running' || appConfigure.runStateBusy && !(appConfigure.app | installationActive)" disabled ><i class="fa fa-refresh fa-spin"></i></button>
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-click="stopApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'running' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-pause"></i> Stop</button>
|
||||
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal restore app -->
|
||||
<div class="modal fade" id="appRestoreModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Really Restore {{ appRestore.app.location }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Restoring the app will lose all content generated since last backup of this app!</p>
|
||||
<fieldset>
|
||||
<form class="form-signin" role="form" name="appRestoreForm" ng-submit="doRestore()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appRestoreForm.password.$dirty && appRestore.error.password) || (appRestoreForm.password.$dirty && appRestoreForm.password.$invalid) }">
|
||||
<label class="control-label" for="appRestorePasswordInput">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="appRestore.password" id="appRestorePasswordInput" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="appRestoreForm.$invalid || busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doRestore()" ng-disabled="appRestoreForm.$invalid || appRestore.busy"><i class="fa fa-spinner fa-pulse" ng-show="appRestore.busy"></i> Restore</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal uninstall app -->
|
||||
<div class="modal fade" id="appUninstallModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Really uninstall {{ appUninstall.app.location }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Deleting the app will also remove all content generated within this app!</p>
|
||||
<fieldset>
|
||||
<form class="form-signin" role="form" name="appUninstallForm" ng-submit="doUninstall()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appUninstallForm.password.$dirty && appUninstall.error.password) || (appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) }">
|
||||
<label class="control-label" for="appUninstallPasswordInput">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="appUninstall.password" id="appUninstallPasswordInput" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
</div>
|
||||
|
||||
<input class="ng-hide" type="submit" ng-disabled="appUninstallForm.$invalid || busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doUninstall()" ng-disabled="appUninstallForm.$invalid || appUninstall.busy"><i class="fa fa-spinner fa-pulse" ng-show="appUninstall.busy"></i> Uninstall</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal update app -->
|
||||
<div class="modal fade" id="appUpdateModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Update {{ appUpdate.app.location }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form class="form-signin" role="form" name="appUpdateForm" ng-submit="doUpdate(appUpdateForm)" autocomplete="off">
|
||||
<div ng-repeat="(env, info) in appUpdate.portBindingsInfo" ng-class="{ 'newPort': info.isNew }">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid }">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appUpdate.portBindingsEnabled[env]"> <span ng-show="info.isNew">New - </span> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
|
||||
<input type="number" class="form-control" ng-model="appUpdate.portBindings[env]" ng-disabled="!appUpdate.portBindingsEnabled[env]" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
<div ng-repeat="(env, port) in appUpdate.obsoletePortBindings" class="obsoletePort">
|
||||
<ng-form name="obsoletePortInfo_form">
|
||||
<div class="form-group">
|
||||
Obsolete -
|
||||
<label class="control-label">{{ env }}</label>
|
||||
<input type="number" class="form-control" ng-model="port" disabled>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appUpdateForm.password.$dirty && appUpdate.error.password) || (appUpdateForm.password.$dirty && appUpdateForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputUpdatePassword">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appUpdateForm.$invalid || busy"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doUpdate(appUpdateForm)" ng-disabled="appUpdateForm.$invalid || appUpdate.busy"><i class="fa fa-spinner fa-pulse" ng-show="appUpdate.busy"></i> Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function imageErrorHandler(elem) {
|
||||
'use strict';
|
||||
|
||||
var appstoreIconUrl = elem.getAttribute('appstore-icon');
|
||||
var fallbackIconUrl = elem.getAttribute('fallback-icon');
|
||||
|
||||
if (elem.src === appstoreIconUrl) {
|
||||
elem.src = fallbackIconUrl;
|
||||
elem.onerror = null; // avoid retry after default icon cannot be loaded
|
||||
} else {
|
||||
elem.src = appstoreIconUrl;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="col-lg-12">
|
||||
<h1>Installed Applications</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="col-sm-1 grid-item" ng-repeat="app in installedApps">
|
||||
<div style="background-color: white;" class="highlight grid-item-content">
|
||||
<a ng-href="{{app | applicationLink}}" target="_blank">
|
||||
<div class="grid-item-top">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
||||
<img ng-src="{{app.iconUrl}}" fallback-icon="img/appicon_fallback.png" appstore-icon="{{ app.iconUrlStore }}" onerror="imageErrorHandler(this)" class="app-icon"/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-left">
|
||||
<div style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">{{ app.location || app.fqdn }}</div>
|
||||
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<div ng-show="app | installationActive">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="grid-item-bottom" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-left">
|
||||
<a href="" ng-click="showRestore(app)" ng-show="(app | installError) === true">
|
||||
<i class="fa fa-undo scale"></i>
|
||||
</a>
|
||||
|
||||
<a href="" ng-click="showConfigure(app)" ng-show="(app | installSuccess) == true">
|
||||
<i class="fa fa-wrench scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-4 text-center">
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<a href="" ng-click="showUpdate(app)" class="ng-hide animateMe" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<i class="fa fa-arrow-up text-success scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-4 text-right">
|
||||
<a href="" ng-click="showUninstall(app)">
|
||||
<i class="fa fa-remove scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
342
webadmin/src/views/apps.js
Normal file
342
webadmin/src/views/apps.js
Normal file
@@ -0,0 +1,342 @@
|
||||
/* global ISTATES:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('AppsController', ['$scope', '$location', 'Client', 'AppStore', function ($scope, $location, Client, AppStore) {
|
||||
$scope.HOST_PORT_MIN = 1024;
|
||||
$scope.HOST_PORT_MAX = 65535;
|
||||
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
|
||||
$scope.appConfigure = {
|
||||
busy: false,
|
||||
error: {},
|
||||
app: {},
|
||||
location: '',
|
||||
password: '',
|
||||
portBindings: {},
|
||||
portBindingsEnabled: {},
|
||||
portBindingsInfo: {},
|
||||
accessRestriction: ''
|
||||
};
|
||||
|
||||
$scope.appUninstall = {
|
||||
busy: false,
|
||||
error: {},
|
||||
app: {},
|
||||
password: ''
|
||||
};
|
||||
|
||||
$scope.appRestore = {
|
||||
busy: false,
|
||||
error: {},
|
||||
app: {},
|
||||
password: ''
|
||||
};
|
||||
|
||||
$scope.appUpdate = {
|
||||
busy: false,
|
||||
error: {},
|
||||
app: {},
|
||||
password: '',
|
||||
manifest: {},
|
||||
portBindings: {}
|
||||
};
|
||||
|
||||
$scope.reset = function () {
|
||||
// reset configure dialog
|
||||
$scope.appConfigure.error = {};
|
||||
$scope.appConfigure.app = {};
|
||||
$scope.appConfigure.location = '';
|
||||
$scope.appConfigure.password = '';
|
||||
$scope.appConfigure.portBindings = {};
|
||||
$scope.appConfigure.accessRestriction = '';
|
||||
|
||||
$scope.appConfigureForm.$setPristine();
|
||||
$scope.appConfigureForm.$setUntouched();
|
||||
|
||||
// reset uninstall dialog
|
||||
$scope.appUninstall.app = {};
|
||||
$scope.appUninstall.error = {};
|
||||
$scope.appUninstall.password = '';
|
||||
|
||||
$scope.appUninstallForm.$setPristine();
|
||||
$scope.appUninstallForm.$setUntouched();
|
||||
|
||||
// reset update dialog
|
||||
$scope.appUpdate.error = {};
|
||||
$scope.appUpdate.app = {};
|
||||
$scope.appUpdate.password = '';
|
||||
$scope.appUpdate.manifest = {};
|
||||
$scope.appUpdate.portBindings = {};
|
||||
|
||||
$scope.appUpdateForm.$setPristine();
|
||||
$scope.appUpdateForm.$setUntouched();
|
||||
|
||||
// reset restore dialog
|
||||
$scope.appRestore.error = {};
|
||||
$scope.appRestore.app = {};
|
||||
$scope.appRestore.password = '';
|
||||
|
||||
$scope.appRestoreForm.$setPristine();
|
||||
$scope.appRestoreForm.$setUntouched();
|
||||
};
|
||||
|
||||
$scope.showConfigure = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
$scope.appConfigure.app = app;
|
||||
$scope.appConfigure.location = app.location;
|
||||
$scope.appConfigure.accessRestriction = app.accessRestriction;
|
||||
$scope.appConfigure.portBindingsInfo = app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope.appConfigure.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appConfigure.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
|
||||
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
|
||||
for (var env in $scope.appConfigure.portBindingsInfo) {
|
||||
if (app.portBindings && app.portBindings[env]) {
|
||||
$scope.appConfigure.portBindings[env] = app.portBindings[env];
|
||||
$scope.appConfigure.portBindingsEnabled[env] = true;
|
||||
} else {
|
||||
$scope.appConfigure.portBindings[env] = $scope.appConfigure.portBindingsInfo[env].defaultValue || 0;
|
||||
$scope.appConfigure.portBindingsEnabled[env] = false;
|
||||
}
|
||||
}
|
||||
|
||||
$('#appConfigureModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.doConfigure = function () {
|
||||
$scope.appConfigure.busy = true;
|
||||
$scope.appConfigure.error.other = null;
|
||||
$scope.appConfigure.error.location = null;
|
||||
$scope.appConfigure.error.password = null;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appConfigure.portBindings) {
|
||||
if ($scope.appConfigure.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appConfigure.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
Client.configureApp($scope.appConfigure.app.id, $scope.appConfigure.password, { location: $scope.appConfigure.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appConfigure.accessRestriction }, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appConfigure.error.port = error.message;
|
||||
} else if (error.statusCode === 409) {
|
||||
$scope.appConfigure.error.location = 'This name is already taken.';
|
||||
$scope.appConfigureForm.location.$setPristine();
|
||||
$('#appConfigureLocationInput').focus();
|
||||
} else if (error.statusCode === 403) {
|
||||
$scope.appConfigure.error.password = 'Wrong password provided.';
|
||||
$scope.appConfigure.password = '';
|
||||
$('#appConfigurePasswordInput').focus();
|
||||
} else {
|
||||
$scope.appConfigure.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appConfigure.busy = false;
|
||||
|
||||
$('#appConfigureModal').modal('hide');
|
||||
|
||||
$scope.reset();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showRestore = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
$scope.appRestore.app = app;
|
||||
|
||||
$('#appRestoreModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.doRestore = function () {
|
||||
$scope.appRestore.busy = true;
|
||||
$scope.appRestore.error.password = null;
|
||||
|
||||
Client.restoreApp($scope.appRestore.app.id, $scope.appRestore.password, function (error) {
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.appRestore.password = '';
|
||||
$scope.appRestore.error.password = true;
|
||||
$('#appRestorePasswordInput').focus();
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$('#appRestoreModal').modal('hide');
|
||||
$scope.reset();
|
||||
}
|
||||
|
||||
$scope.appRestore.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showUninstall = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
$scope.appUninstall.app = app;
|
||||
|
||||
$('#appUninstallModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.doUninstall = function () {
|
||||
$scope.appUninstall.busy = true;
|
||||
$scope.appUninstall.error.password = null;
|
||||
|
||||
Client.uninstallApp($scope.appUninstall.app.id, $scope.appUninstall.password, function (error) {
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.appUninstall.password = '';
|
||||
$scope.appUninstall.error.password = true;
|
||||
$('#appUninstallPasswordInput').focus();
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$('#appUninstallModal').modal('hide');
|
||||
$scope.reset();
|
||||
}
|
||||
|
||||
$scope.appUninstall.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showUpdate = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
$scope.appUpdate.app = app;
|
||||
|
||||
AppStore.getManifest(app.appStoreId, function (error, manifest) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.appUpdate.manifest = manifest;
|
||||
|
||||
// ensure we always operate on objects here
|
||||
app.portBindings = app.portBindings || {};
|
||||
app.manifest.tcpPorts = app.manifest.tcpPorts || {};
|
||||
manifest.tcpPorts = manifest.tcpPorts || {};
|
||||
|
||||
// Activate below two lines for testing the UI
|
||||
// manifest.tcpPorts['TEST_HTTP'] = { defaultValue: 1337, description: 'HTTP server'};
|
||||
// app.manifest.tcpPorts['TEST_FOOBAR'] = { defaultValue: 1338, description: 'FOOBAR server'};
|
||||
// app.portBindings['TEST_SSH'] = 1339;
|
||||
|
||||
var portBindingsInfo = {}; // Portbinding map only for information
|
||||
var portBindings = {}; // This is the actual model holding the env:port pair
|
||||
var portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
var obsoletePortBindings = {}; // Info map for obsolete port bindings, this is for display use only and thus not in the model
|
||||
var portsChanged = false;
|
||||
var env;
|
||||
|
||||
// detect new portbindings and copy all from manifest.tcpPorts
|
||||
for (env in manifest.tcpPorts) {
|
||||
portBindingsInfo[env] = manifest.tcpPorts[env];
|
||||
if (!app.manifest.tcpPorts[env]) {
|
||||
portBindingsInfo[env].isNew = true;
|
||||
portBindingsEnabled[env] = true;
|
||||
|
||||
// use default integer port value in model
|
||||
portBindings[env] = manifest.tcpPorts[env].defaultValue || 0;
|
||||
|
||||
portsChanged = true;
|
||||
} else {
|
||||
// detect if the port binding was enabled
|
||||
if (app.portBindings[env]) {
|
||||
portBindings[env] = app.portBindings[env];
|
||||
portBindingsEnabled[env] = true;
|
||||
} else {
|
||||
portBindings[env] = manifest.tcpPorts[env].defaultValue || 0;
|
||||
portBindingsEnabled[env] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detect obsolete portbindings (mappings in app.portBindings, but not anymore in manifest.tcpPorts)
|
||||
for (env in app.manifest.tcpPorts) {
|
||||
// only list the port if it is not in the new manifest and was enabled previously
|
||||
if (!manifest.tcpPorts[env] && app.portBindings[env]) {
|
||||
obsoletePortBindings[env] = app.portBindings[env];
|
||||
portsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// now inject the maps into the $scope, we only show those if ports have changed
|
||||
$scope.appUpdate.portBindings = portBindings; // always inject the model, so it gets used in the actual update call
|
||||
$scope.appUpdate.portBindingsEnabled = portBindingsEnabled; // always inject the model, so it gets used in the actual update call
|
||||
|
||||
if (portsChanged) {
|
||||
$scope.appUpdate.portBindingsInfo = portBindingsInfo;
|
||||
$scope.appUpdate.obsoletePortBindings = obsoletePortBindings;
|
||||
} else {
|
||||
$scope.appUpdate.portBindingsInfo = {};
|
||||
$scope.appUpdate.obsoletePortBindings = {};
|
||||
}
|
||||
|
||||
$('#appUpdateModal').modal('show');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doUpdate = function (form) {
|
||||
$scope.appUpdate.error.password = null;
|
||||
$scope.appUpdate.busy = true;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appUpdate.portBindings) {
|
||||
if ($scope.appUpdate.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appUpdate.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
Client.updateApp($scope.appUpdate.app.id, $scope.appUpdate.manifest, finalPortBindings, $scope.appUpdate.password, function (error) {
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.appUpdate.password = '';
|
||||
$scope.appUpdate.error.password = true;
|
||||
} else if (error) {
|
||||
Client.error(error);
|
||||
} else {
|
||||
$scope.appUpdate.app = {};
|
||||
$scope.appUpdate.password = '';
|
||||
|
||||
form.$setPristine();
|
||||
form.$setUntouched();
|
||||
|
||||
$('#appUpdateModal').modal('hide');
|
||||
}
|
||||
|
||||
$scope.appUpdate.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.startApp = function (app) {
|
||||
$scope.appConfigure.runStateBusy = true;
|
||||
app.runState = 'pending_start'; // we assume we will end up there
|
||||
Client.startApp(app.id, function () {
|
||||
$scope.appConfigure.runStateBusy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.stopApp = function (app) {
|
||||
$scope.appConfigure.runStateBusy = true;
|
||||
app.runState = 'pending_stop'; // we assume we will end up there
|
||||
Client.stopApp(app.id, function () {
|
||||
$scope.appConfigure.runStateBusy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['appConfigureModal', 'appUninstallModal', 'appUpdateModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
124
webadmin/src/views/appstore.html
Normal file
124
webadmin/src/views/appstore.html
Normal file
@@ -0,0 +1,124 @@
|
||||
|
||||
<!-- Modal install app -->
|
||||
<div class="modal fade" id="appInstallModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="appInstallModalLabel">
|
||||
<img ng-src="{{appInstall.app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
{{ appInstall.app.manifest.title }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="collapse" id="collapseInstallForm" data-toggle="false">
|
||||
<form class="form-signin" role="form" name="appInstallForm" ng-submit="doInstall()" autocomplete="off">
|
||||
<div class="has-error text-center" ng-show="appInstall.error.other" ng-bind-html="appInstall.error.other"></div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (appInstallForm.location.$dirty && appInstallForm.location.$invalid) || (!appInstallForm.location.$dirty && appInstall.error.location) }">
|
||||
<label class="control-label" for="appInstallLocationInput">Location {{ appInstall.error.location }} </label>
|
||||
<div class="input-group form-inline">
|
||||
<input type="text" class="form-control" ng-model="appInstall.location" id="appInstallLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
|
||||
<div class="input-group-addon">
|
||||
{{ !appInstall.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="has-error text-center" ng-show="appInstall.error.port">{{ appInstall.error.port }}</div>
|
||||
<div ng-repeat="(env, info) in appInstall.portBindingsInfo">
|
||||
<ng-form name="portInfo_form">
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appInstallForm.itemName{{$index}}.$dirty && appInstall.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
||||
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appInstall.portBindingsEnabled[env]"> {{ info.description }} ({{ hostPortMin }} - {{ hostPortMax }})</label>
|
||||
<input type="number" class="form-control" ng-model="appInstall.portBindings[env]" ng-disabled="!appInstall.portBindingsEnabled[env]" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
|
||||
</div>
|
||||
</ng-form>
|
||||
</div>
|
||||
<!--
|
||||
<div class="form-group">
|
||||
<label class="control-label" for="accessRestriction">Access Restriction</label>
|
||||
<select class="form-control" id="accessRestriction" ng-model="appInstall.accessRestriction">
|
||||
<option value="">None</option>
|
||||
<option value="roleUser">Only for Cloudron Users</option>
|
||||
</select>
|
||||
</div>
|
||||
-->
|
||||
<input class="ng-hide" type="submit" ng-disabled="appInstallForm.$invalid || busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="collapse" id="collapseMediaLinksCarousel" data-toggle="false">
|
||||
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');" ng-show="appInstall.mediaLinks.length == 1"></div>
|
||||
<slick init-onload="true" current-index="0" autoplay="true" arrows="false" autoplay-speed="2000" data="appInstall.mediaLinks" ng-show="appInstall.mediaLinks.length > 1">
|
||||
<div ng-repeat="mediaLink in appInstall.mediaLinks" class="slick-item" style="background-image: url('{{mediaLink}}');"></div>
|
||||
</slick>
|
||||
<div class="appstore-install-description">
|
||||
<div ng-bind-html="appInstall.app.manifest.description | markdown2html"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-success" ng-show="!appInstall.installFormVisible && user.admin" ng-click="showInstallForm()">Install</button>
|
||||
<button type="button" class="btn btn-success" ng-show="appInstall.installFormVisible && user.admin" ng-click="doInstall()" ng-disabled="appInstallForm.$invalid || appInstall.busy"><i class="fa fa-spinner fa-pulse" ng-show="appInstall.busy"></i> Install</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="row-no-margin">
|
||||
<div class="col-md-2">
|
||||
</div>
|
||||
<div class="col-md-8 col-same-height">
|
||||
<br/>
|
||||
<h1>Available Applications</h1>
|
||||
<br/>
|
||||
</div>
|
||||
<div class="col-md-2 col-same-height" style="float: right;">
|
||||
<br/>
|
||||
<div class="appstore-search">
|
||||
<form ng-submit="search()">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Search for..." ng-model="searchString" ng-change="search()">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" type="submit" ng-click="search()"><i class="fa fa-search"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === '' }" category="">All</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'blog' }" category="blog">Blog</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'chat' }" category="chat">Chat</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'sync' }" category="sync">Media Sync</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'git' }" category="git">Code Hosting</a>
|
||||
<a href="" class="appstore-category-link" ng-click="showCategory($event);" ng-class="{'category-active': category === 'wiki' }" category="wiki">Wiki</a>
|
||||
</div>
|
||||
<div class="col-md-10" ng-show="ready && apps.length">
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-1 appstore-item" ng-repeat="app in apps">
|
||||
<div class="appstore-item-content highlight" ng-click="showInstall(app)">
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
<div class="appstore-item-content-description col-same-height">
|
||||
<h4 class="appstore-item-content-title">{{ app.manifest.title }}</h4>
|
||||
<div class="appstore-item-content-tagline text-muted">{{ app.manifest.tagline }}</div>
|
||||
<!-- <div class="appstore-item-rating"><i class="fa fa-star"></i><i class="fa fa-star"></i><i class="fa fa-star"></i><i class="fa fa-star-half-o"></i><i class="fa fa-star-o"></i></div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-10 animateMeOpacity loading-banner" ng-show="ready && !apps.length">
|
||||
<h3 class="text-muted">No applications in this category</h3>
|
||||
</div>
|
||||
<div class="col-md-10 animateMeOpacity loading-banner" ng-show="!ready">
|
||||
<h2><i class="fa fa-spinner fa-pulse"></i> Loading</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offset the footer -->
|
||||
<br/><br/>
|
||||
185
webadmin/src/views/appstore.js
Normal file
185
webadmin/src/views/appstore.js
Normal file
@@ -0,0 +1,185 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('AppStoreController', ['$scope', '$location', '$timeout', '$routeParams', 'Client', 'AppStore', function ($scope, $location, $timeout, $routeParams, Client, AppStore) {
|
||||
$scope.ready = false;
|
||||
$scope.apps = [];
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.category = '';
|
||||
$scope.cachedCategory = ''; // used to cache the selected category while searching
|
||||
$scope.searchString = '';
|
||||
|
||||
$scope.appInstall = {
|
||||
busy: false,
|
||||
installFormVisible: false,
|
||||
error: {},
|
||||
app: {},
|
||||
location: '',
|
||||
portBindings: {},
|
||||
accessRestriction: '',
|
||||
mediaLinks: []
|
||||
};
|
||||
|
||||
$scope.search = function () {
|
||||
if (!$scope.searchString) return $scope.showCategory(null, $scope.cachedCategory);
|
||||
|
||||
$scope.category = '';
|
||||
|
||||
AppStore.getAppsFast(function (error, apps) {
|
||||
if (error) return $timeout($scope.search, 1000);
|
||||
|
||||
var token = $scope.searchString.toUpperCase();
|
||||
|
||||
$scope.apps = apps.filter(function (app) {
|
||||
if (app.manifest.id.toUpperCase().indexOf(token) !== -1) return true;
|
||||
if (app.manifest.title.toUpperCase().indexOf(token) !== -1) return true;
|
||||
if (app.manifest.tagline.toUpperCase().indexOf(token) !== -1) return true;
|
||||
if (app.manifest.tags.join().toUpperCase().indexOf(token) !== -1) return true;
|
||||
if (app.manifest.description.toUpperCase().indexOf(token) !== -1) return true;
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showCategory = function (event, category) {
|
||||
if (!event) $scope.category = category;
|
||||
else $scope.category = event.target.getAttribute('category');
|
||||
|
||||
$scope.cachedCategory = $scope.category;
|
||||
|
||||
$scope.ready = false;
|
||||
|
||||
AppStore.getApps(function (error, apps) {
|
||||
if (error) return $timeout($scope.showCategory.bind(null, event), 1000);
|
||||
|
||||
if (!$scope.category) {
|
||||
$scope.apps = apps;
|
||||
} else {
|
||||
$scope.apps = apps.filter(function (app) {
|
||||
return app.manifest.tags.some(function (tag) { return $scope.category === tag; });
|
||||
});
|
||||
}
|
||||
|
||||
$scope.ready = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.reset = function () {
|
||||
$scope.appInstall.app = {};
|
||||
$scope.appInstall.error = {};
|
||||
$scope.appInstall.location = '';
|
||||
$scope.appInstall.portBindings = {};
|
||||
$scope.appInstall.accessRestriction = '';
|
||||
$scope.appInstall.installFormVisible = false;
|
||||
$scope.appInstall.mediaLinks = [];
|
||||
$('#collapseInstallForm').collapse('hide');
|
||||
$('#collapseMediaLinksCarousel').collapse('show');
|
||||
|
||||
$scope.appInstallForm.$setPristine();
|
||||
$scope.appInstallForm.$setUntouched();
|
||||
};
|
||||
|
||||
$scope.showInstallForm = function () {
|
||||
$scope.appInstall.installFormVisible = true;
|
||||
$('#collapseMediaLinksCarousel').collapse('hide');
|
||||
$('#collapseInstallForm').collapse('show');
|
||||
$('#appInstallLocationInput').focus();
|
||||
};
|
||||
|
||||
$scope.showInstall = function (app) {
|
||||
$scope.reset();
|
||||
|
||||
// make a copy to work with in case the app object gets updated while polling
|
||||
angular.copy(app, $scope.appInstall.app);
|
||||
$('#appInstallModal').modal('show');
|
||||
|
||||
$scope.appInstall.mediaLinks = $scope.appInstall.app.manifest.mediaLinks || [];
|
||||
$scope.appInstall.location = app.location;
|
||||
$scope.appInstall.portBindingsInfo = $scope.appInstall.app.manifest.tcpPorts || {}; // Portbinding map only for information
|
||||
$scope.appInstall.portBindings = {}; // This is the actual model holding the env:port pair
|
||||
$scope.appInstall.portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
|
||||
$scope.appInstall.accessRestriction = app.accessRestriction || '';
|
||||
|
||||
// set default ports
|
||||
for (var env in $scope.appInstall.app.manifest.tcpPorts) {
|
||||
$scope.appInstall.portBindings[env] = $scope.appInstall.app.manifest.tcpPorts[env].defaultValue || 0;
|
||||
$scope.appInstall.portBindingsEnabled[env] = true;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
$scope.doInstall = function () {
|
||||
$scope.appInstall.busy = true;
|
||||
$scope.appInstall.error.other = null;
|
||||
$scope.appInstall.error.location = null;
|
||||
$scope.appInstall.error.port = null;
|
||||
|
||||
// only use enabled ports from portBindings
|
||||
var finalPortBindings = {};
|
||||
for (var env in $scope.appInstall.portBindings) {
|
||||
if ($scope.appInstall.portBindingsEnabled[env]) {
|
||||
finalPortBindings[env] = $scope.appInstall.portBindings[env];
|
||||
}
|
||||
}
|
||||
|
||||
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, { location: $scope.appInstall.location || '', portBindings: finalPortBindings, accessRestriction: $scope.appInstall.accessRestriction }, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
|
||||
$scope.appInstall.error.port = error.message;
|
||||
} else if (error.statusCode === 409) {
|
||||
$scope.appInstall.error.location = 'This name is already taken.';
|
||||
$scope.appInstallForm.location.$setPristine();
|
||||
$('#appInstallLocationInput').focus();
|
||||
} else if (error.statusCode === 402) {
|
||||
$scope.appInstall.error.other = 'Unable to purchase this app<br/>Please make sure your payment is setup <a href="' + $scope.config.webServerOrigin + '/console.html#/userprofile" target="_blank">here</a>';
|
||||
} else {
|
||||
$scope.appInstall.error.other = error.message;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.appInstall.busy = false;
|
||||
|
||||
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
|
||||
$('#appInstallModal').on('hidden.bs.modal', function () {
|
||||
$scope.reset();
|
||||
$location.path('/apps');
|
||||
});
|
||||
|
||||
$('#appInstallModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
$scope.ready = false;
|
||||
|
||||
AppStore.getApps(function (error, apps) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return $timeout(refresh, 1000);
|
||||
}
|
||||
|
||||
$scope.apps = apps;
|
||||
|
||||
// show install app dialog immediately if an app id was passed in the query
|
||||
if ($routeParams.appId) {
|
||||
var found = apps.filter(function (app) { return (app.id === $routeParams.appId); });
|
||||
if (found.length) $scope.showInstall(found[0]);
|
||||
}
|
||||
|
||||
$scope.ready = true;
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['appInstallModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
46
webadmin/src/views/dns.html
Normal file
46
webadmin/src/views/dns.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h1>DNS Management</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 grid-item">
|
||||
<div class="grid-item-content">
|
||||
<div class="grid-item-top">
|
||||
<big>Certificate</big>
|
||||
</div>
|
||||
<div class="grid-item-bottom text-right">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<input type="file" id="idCertificate" style="display:none"/>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" onclick="getElementById('idCertificate').click();">Certificate</button>
|
||||
</span>
|
||||
<input type="text" class="form-control" ng-model="certificateFileName" onclick="getElementById('idCertificate').click();" style="cursor: pointer;"/>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('idCertificate').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input type="file" id="idKey" style="display:none"/>
|
||||
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="button" onclick="getElementById('idKey').click();">Key</button>
|
||||
</span>
|
||||
<input type="text" class="form-control" ng-model="keyFileName" onclick="getElementById('idKey').click();" style="cursor: pointer;"/>
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-upload" onclick="getElementById('idKey').click();"></i>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<button class="btn btn-outline btn-success" ng-click="setCertificate()">Upload Certificate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
35
webadmin/src/views/dns.js
Normal file
35
webadmin/src/views/dns.js
Normal file
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('DnsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.certificateFile = null;
|
||||
$scope.certificateFileName = '';
|
||||
$scope.keyFile = null;
|
||||
$scope.keyFileName = '';
|
||||
|
||||
document.getElementById('idCertificate').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.certificateFile = event.target.files[0];
|
||||
$scope.certificateFileName = event.target.files[0].name;
|
||||
});
|
||||
};
|
||||
|
||||
document.getElementById('idKey').onchange = function (event) {
|
||||
$scope.$apply(function () {
|
||||
$scope.keyFile = event.target.files[0];
|
||||
$scope.keyFileName = event.target.files[0].name;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setCertificate = function () {
|
||||
if (!$scope.certificateFile) return console.log('Certificate not set');
|
||||
if (!$scope.keyFile) return console.log('Key not set');
|
||||
|
||||
Client.setCertificate($scope.certificateFile, $scope.keyFile, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
window.setTimeout(window.location.reload.bind(window.location, true), 3000);
|
||||
});
|
||||
};
|
||||
}]);
|
||||
103
webadmin/src/views/graphs.html
Normal file
103
webadmin/src/views/graphs.html
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>Graphs</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="graphs">
|
||||
|
||||
<div class="row shadow memory-app-container">
|
||||
<h2>Disk Usage</h2>
|
||||
<br/>
|
||||
<div class="col-md-4">
|
||||
<h4>Applications <span class="badge">{{ diskUsage['docker'].sum }} GB</span></h4>
|
||||
<canvas id="dockerDiskUsageChart" width="200" height="200"></canvas>
|
||||
<p>
|
||||
<span class="text-success">Free {{ diskUsage['docker'].free }} GB</span>
|
||||
|
||||
<span class="text-warning">Reserved {{ diskUsage['docker'].reserved }} GB</span>
|
||||
|
||||
<span class="text-primary">Used {{ diskUsage['docker'].used }} GB</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4>Data <span class="badge">{{ diskUsage['box'].sum }} GB</span></h4>
|
||||
<canvas id="boxDiskUsageChart" width="200" height="200"></canvas>
|
||||
<p>
|
||||
<span class="text-success">Free {{ diskUsage['box'].free }} GB</span>
|
||||
|
||||
<span class="text-warning">Reserved {{ diskUsage['box'].reserved }} GB</span>
|
||||
|
||||
<span class="text-primary">Used {{ diskUsage['box'].used }} GB</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4>System (all) <span class="badge">{{ diskUsage['cloudron'].sum }} GB</span></h4>
|
||||
<canvas id="cloudronDiskUsageChart" width="200" height="200"></canvas>
|
||||
<p>
|
||||
<span class="text-success">Free {{ diskUsage['cloudron'].free }} GB</span>
|
||||
|
||||
<span class="text-warning">Reserved {{ diskUsage['cloudron'].reserved }} GB</span>
|
||||
|
||||
<span class="text-primary">Used {{ diskUsage['cloudron'].used }} GB</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="row shadow memory-app-container">
|
||||
<div class="col-md-12">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>Memory</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-2">
|
||||
<h4>Apps</h4>
|
||||
<canvas id="memoryUsageAppsChart" width="200" height="200"></canvas>
|
||||
<p>
|
||||
<span ng-repeat="data in memoryUsageApps">
|
||||
<span style="color: {{data.color}};" class="memory-chart-label">{{ data.label }} {{ data.value }} MB</span>
|
||||
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4>System</h4>
|
||||
<canvas id="memoryUsageSystemChart" width="200" height="200"></canvas>
|
||||
<p>
|
||||
<span ng-repeat="data in memoryUsageSystem">
|
||||
<span style="color: {{data.color}};" class="memory-chart-label">{{ data.label }} {{ data.value }} MB</span>
|
||||
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h4 ng-show="activeApp === 'system'">System</h4>
|
||||
<h4 ng-show="activeApp !== 'system'">{{ activeApp.location }}</h4>
|
||||
</ul>
|
||||
</div>
|
||||
<br/>
|
||||
<canvas id="memoryAppChart" width="800" height="300"></canvas>
|
||||
<p>Memory consumption over time in MB.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
211
webadmin/src/views/graphs.js
Normal file
211
webadmin/src/views/graphs.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/* global Chart:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('GraphsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.diskUsage = {};
|
||||
$scope.memoryUsageSystem = [];
|
||||
$scope.memoryUsageApps = [];
|
||||
$scope.activeApp = null;
|
||||
$scope.memoryChart = null;
|
||||
|
||||
$scope.installedApps = Client.getInstalledApps();
|
||||
|
||||
function bytesToGigaBytes(value) {
|
||||
return (value/1024/1024/1024).toFixed(2);
|
||||
}
|
||||
|
||||
function bytesToMegaBytes(value) {
|
||||
return (value/1024/1024).toFixed(2);
|
||||
}
|
||||
|
||||
// http://stackoverflow.com/questions/1484506/random-color-generator-in-javascript
|
||||
function getRandomColor() {
|
||||
var letters = '0123456789ABCDEF'.split('');
|
||||
var color = '#';
|
||||
for (var i = 0; i < 6; i++ ) {
|
||||
color += letters[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
function renderDisk(type, free, reserved, used) {
|
||||
$scope.diskUsage[type] = {
|
||||
used: bytesToGigaBytes(used.datapoints[0][0]),
|
||||
reserved: bytesToGigaBytes(reserved.datapoints[0][0]),
|
||||
free: bytesToGigaBytes(free.datapoints[0][0]),
|
||||
sum: bytesToGigaBytes(used.datapoints[0][0] + reserved.datapoints[0][0] + free.datapoints[0][0])
|
||||
};
|
||||
|
||||
var tmp = [{
|
||||
value: $scope.diskUsage[type].used,
|
||||
color: "#2196F3",
|
||||
highlight: "#82C4F8",
|
||||
label: "Used"
|
||||
}, {
|
||||
value: $scope.diskUsage[type].reserved,
|
||||
color: "#f0ad4e",
|
||||
highlight: "#F8D9AC",
|
||||
label: "Reserved"
|
||||
}, {
|
||||
value: $scope.diskUsage[type].free,
|
||||
color:"#27CE65",
|
||||
highlight: "#76E59F",
|
||||
label: "Free"
|
||||
}];
|
||||
|
||||
var ctx = $('#' + type + 'DiskUsageChart').get(0).getContext('2d');
|
||||
var myChart = new Chart(ctx);
|
||||
myChart.Doughnut(tmp);
|
||||
}
|
||||
|
||||
$scope.setMemoryApp = function (app, color) {
|
||||
$scope.activeApp = app;
|
||||
|
||||
var timePeriod = 2 * 60; // in minutes
|
||||
var timeBucketSize = 30; // in minutes
|
||||
|
||||
var target;
|
||||
if (app === 'system') target = 'summarize(collectd.localhost.memory.memory-used, "' + timeBucketSize + 'min", "avg")';
|
||||
else target = 'summarize(collectd.localhost.table-' + app.id + '-memory.gauge-rss, "' + timeBucketSize + 'min", "avg")';
|
||||
|
||||
Client.graphs([target], '-' + timePeriod + 'min', function (error, result) {
|
||||
if (error) return console.log(error);
|
||||
|
||||
// translate the data from bytes to MB
|
||||
var data = result[0].datapoints.map(function (d) { return parseInt((d[0] / 1024 / 1024).toFixed(2)); });
|
||||
var labels = data.map(function (d, index) { return '-' + (timePeriod - (index * timeBucketSize)) / 60 + 'h'; });
|
||||
|
||||
var tmp = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'Memory',
|
||||
fillColor: color || "#82C4F8",
|
||||
strokeColor: color || "#2196F3",
|
||||
pointColor: color || "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#ffffff",
|
||||
pointHighlightFill: color || "#82C4F8",
|
||||
pointHighlightStroke: color || "#82C4F8",
|
||||
data: data
|
||||
}]
|
||||
};
|
||||
|
||||
var ctx = $('#memoryAppChart').get(0).getContext('2d');
|
||||
var chart = new Chart(ctx);
|
||||
|
||||
var options = {
|
||||
scaleOverride: true,
|
||||
scaleSteps: 10,
|
||||
scaleStepWidth: $scope.activeApp === 'system' ? 100 : 10,
|
||||
scaleStartValue: 0
|
||||
};
|
||||
|
||||
if ($scope.memoryChart) $scope.memoryChart.destroy();
|
||||
$scope.memoryChart = chart.Line(tmp, options);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateDiskGraphs = function () {
|
||||
Client.graphs([
|
||||
'averageSeries(collectd.localhost.df-loop0.df_complex-free)',
|
||||
'averageSeries(collectd.localhost.df-loop0.df_complex-reserved)',
|
||||
'averageSeries(collectd.localhost.df-loop0.df_complex-used)',
|
||||
|
||||
'averageSeries(collectd.localhost.df-loop1.df_complex-free)',
|
||||
'averageSeries(collectd.localhost.df-loop1.df_complex-reserved)',
|
||||
'averageSeries(collectd.localhost.df-loop1.df_complex-used)',
|
||||
|
||||
'averageSeries(collectd.localhost.df-vda1.df_complex-free)',
|
||||
'averageSeries(collectd.localhost.df-vda1.df_complex-reserved)',
|
||||
'averageSeries(collectd.localhost.df-vda1.df_complex-used)',
|
||||
], '-1min', function (error, data) {
|
||||
if (error) return console.log(error);
|
||||
|
||||
renderDisk('docker', data[0], data[1], data[2]);
|
||||
renderDisk('box', data[3], data[4], data[5]);
|
||||
renderDisk('cloudron', data[6], data[7], data[8]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateMemorySystemChart = function () {
|
||||
var targets = [];
|
||||
var targetsInfo = [];
|
||||
|
||||
targets.push('summarize(collectd.localhost.memory.memory-used, "1min", "avg")');
|
||||
targetsInfo.push({ label: 'System', color: '#2196F3' });
|
||||
|
||||
targets.push('summarize(sum(collectd.localhost.memory.memory-buffered, collectd.localhost.memory.memory-cached), "1min", "avg")');
|
||||
targetsInfo.push({ label: 'Cached', color: '#f0ad4e' });
|
||||
|
||||
targets.push('summarize(collectd.localhost.memory.memory-free, "1min", "avg")');
|
||||
targetsInfo.push({ label: 'Free', color: '#27CE65' });
|
||||
|
||||
Client.graphs(targets, '-1min', function (error, result) {
|
||||
if (error) return console.log(error);
|
||||
|
||||
$scope.memoryUsageSystem = result.map(function (data, index) {
|
||||
return {
|
||||
value: bytesToMegaBytes(data.datapoints[0][0]),
|
||||
color: targetsInfo[index].color,
|
||||
highlight: targetsInfo[index].color,
|
||||
label: targetsInfo[index].label
|
||||
};
|
||||
});
|
||||
|
||||
var ctx = $('#memoryUsageSystemChart').get(0).getContext('2d');
|
||||
var chart = new Chart(ctx).Doughnut($scope.memoryUsageSystem);
|
||||
|
||||
$('#memoryUsageSystemChart').get(0).onclick = function (event) {
|
||||
$scope.setMemoryApp('system');
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateMemoryAppsChart = function () {
|
||||
var targets = [];
|
||||
var targetsInfo = [];
|
||||
|
||||
$scope.installedApps.forEach(function (app) {
|
||||
targets.push('summarize(collectd.localhost.table-' + app.id + '-memory.gauge-rss, "1min", "avg")');
|
||||
targetsInfo.push({
|
||||
label: app.location,
|
||||
color: getRandomColor(),
|
||||
app: app
|
||||
});
|
||||
});
|
||||
|
||||
Client.graphs(targets, '-1min', function (error, result) {
|
||||
if (error) return console.log(error);
|
||||
|
||||
$scope.memoryUsageApps = result.map(function (data, index) {
|
||||
return {
|
||||
value: bytesToMegaBytes(data.datapoints[0][0]),
|
||||
color: targetsInfo[index].color,
|
||||
highlight: targetsInfo[index].color,
|
||||
label: targetsInfo[index].label
|
||||
};
|
||||
});
|
||||
|
||||
var ctx = $('#memoryUsageAppsChart').get(0).getContext('2d');
|
||||
var chart = new Chart(ctx).Doughnut($scope.memoryUsageApps);
|
||||
|
||||
$('#memoryUsageAppsChart').get(0).onclick = function (event) {
|
||||
var activeBars = chart.getSegmentsAtEvent(event);
|
||||
|
||||
// dismiss non chart clicks
|
||||
if (!activeBars || !activeBars[0]) return;
|
||||
|
||||
// try to find the app for this segment
|
||||
var selectedDataInfo = targetsInfo.filter(function (info) { return info.label === activeBars[0].label; })[0];
|
||||
if (selectedDataInfo) $scope.setMemoryApp(selectedDataInfo.app, selectedDataInfo.color);
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady($scope.updateDiskGraphs);
|
||||
Client.onReady($scope.updateMemorySystemChart);
|
||||
Client.onReady($scope.updateMemoryAppsChart);
|
||||
Client.onReady($scope.setMemoryApp.bind(null, 'system'));
|
||||
}]);
|
||||
197
webadmin/src/views/settings.html
Normal file
197
webadmin/src/views/settings.html
Normal file
@@ -0,0 +1,197 @@
|
||||
<!-- Modal developer mode -->
|
||||
<div class="modal fade" id="developerModeChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" ng-hide="config.developerMode">Enable Developer Mode</h4>
|
||||
<h4 class="modal-title" ng-show="config.developerMode">Disable Developer Mode</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="developerModeChangeForm" class="form-signin" role="form" novalidate ng-submit="doChangeDeveloperMode(developerModeChangeForm)" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (developerModeChangeForm.password.$dirty && developerModeChangeForm.password.$invalid)}">
|
||||
<label class="control-label" for="inputDeveloperModeChangePassword">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!developerModeChangeForm.password.$dirty && developerModeChange.error.password) || (developerModeChangeForm.password.$dirty && developerModeChangeForm.password.$invalid)">
|
||||
<small ng-show="developerModeChangeForm.password.$error.required">A password is required</small>
|
||||
<small ng-show="developerModeChangeForm.password.$error.minlength">The password is too short</small>
|
||||
<small ng-show="developerModeChangeForm.password.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="developerModeChangeForm.password.$dirty && developerModeChange.error.password">Invalid pasword</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="developerModeChange.password" id="inputDeveloperModeChangePassword" name="password" ng-maxlength="512" ng-minlength="5" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="developerModeChangeForm.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-hide="config.developerMode" ng-click="doChangeDeveloperMode(developerModeChangeForm)" ng-disabled="developerModeChangeForm.$invalid || developerModeChange.busy"><i class="fa fa-spinner fa-pulse" ng-show="developerModeChange.busy"></i> Enable</button>
|
||||
<button type="button" class="btn btn-success" ng-show="config.developerMode" ng-click="doChangeDeveloperMode(developerModeChangeForm)" ng-disabled="developerModeChangeForm.$invalid || developerModeChange.busy"><i class="fa fa-spinner fa-pulse" ng-show="developerModeChange.busy"></i> Disable</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change name -->
|
||||
<div class="modal fade" id="nameChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change the Cloudron Name</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="nameChangeForm" class="form-signin" role="form" novalidate ng-submit="doChangeName()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (nameChangeForm.name.$dirty && nameChangeForm.name.$invalid) }">
|
||||
<label class="control-label" for="inputNameChangeName">New Cloudron Name</label>
|
||||
<div class="control-label" ng-show="(!nameChangeForm.name.$dirty && nameChange.error.name) || (nameChangeForm.name.$dirty && nameChangeForm.name.$invalid)">
|
||||
<small ng-show="nameChangeForm.name.$error.required">A valid name is required</small>
|
||||
<small ng-show="(nameChangeForm.name.$dirty && nameChangeForm.name.$invalid) && !nameChangeForm.name.$error.required">The name is not valid</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="nameChange.name" id="inputNameChangeName" name="name" ng-maxlength="512" ng-minlength="1" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="nameChangeForm.$invalid"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doChangeName()" ng-disabled="nameChangeForm.$invalid || nameChange.busy"><i class="fa fa-spinner fa-pulse" ng-show="nameChange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal change avatar -->
|
||||
<div class="modal fade" id="avatarChangeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Change your Cloudron Avatar</h4>
|
||||
</div>
|
||||
<div class="modal-body settings-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{avatarChange.avatar.data || avatarChange.avatar.url || avatar.data || avatar.url}}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/png"/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item" ng-repeat="avatar in avatarChange.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="setPreviewAvatar(avatar)"></div>
|
||||
<div class="item add" ng-click="showCustomAvatarSelector()"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doChangeAvatar()" ng-disabled="avatarChange.busy"><i class="fa fa-spinner fa-pulse" ng-show="avatarChange.busy"></i> Change</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal backup -->
|
||||
<div class="modal fade" id="createBackupModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Create a new Backup</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Do you really want to create a new backup?
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doCreateBackup()" ng-disabled="createBackup.busy"><i class="fa fa-spinner fa-pulse" ng-show="developerModeChange.busy"></i> Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
|
||||
<div class="text-left">
|
||||
<h3>Backups</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<span class="text-muted">Last Backup</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<span ng-show="lastBackup">{{ lastBackup.creationTime | prettyDate }}</span>
|
||||
<span ng-hide="lastBackup">No backups have been created yet</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- If a backup is blocked (not triggered), the UI does not give feedback currently. This button is only available in dev at the moment! -->
|
||||
<div class="row" ng-show="config.isDev">
|
||||
<br/>
|
||||
<div class="col-xs-9">
|
||||
<div ng-show="createBackup.busy" class="progress progress-striped active animateMe">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ createBackup.percent }}%">Backup in progress</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-3 text-right">
|
||||
<button class="btn btn-outline btn-xs btn-primary" ng-click="showCreateBackup()" ng-disabled="createBackup.busy">Create Backup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
|
||||
<div class="text-left">
|
||||
<h3>About</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-4" style="min-width: 150px;">
|
||||
<div class="settings-avatar" ng-click="showChangeAvatar()" style="background-image: url('{{avatar.data || avatar.url}}');">
|
||||
<div class="overlay"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-8">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Name</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.cloudronName }} <a href="" ng-click="showChangeName()"><i class="fa fa-pencil text-small"></i></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Model</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted" style="vertical-align: top;">Version</td>
|
||||
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.version }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
|
||||
<div class="text-left">
|
||||
<h3>Developer Mode</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
The developer mode will enable additional functionality for developers. This mode allows for example the Cloudron commandline tool to control parts of the Cloudron, like installing and debugging applications.
|
||||
<br/>
|
||||
<br/>
|
||||
If you are interested in application development, please visit the <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank">application developer documentation</a>.
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
Status
|
||||
</div>
|
||||
<div class="col-xs-6 text-right">
|
||||
<a href="" class="text-danger" ng-show="config.developerMode" ng-click="showChangeDeveloperMode()">Enabled</a>
|
||||
<a href="" ng-hide="config.developerMode" ng-click="showChangeDeveloperMode()">Disabled</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
284
webadmin/src/views/settings.js
Normal file
284
webadmin/src/views/settings.js
Normal file
@@ -0,0 +1,284 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('SettingsController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
|
||||
$scope.lastBackup = null;
|
||||
$scope.backups = [];
|
||||
$scope.avatar = {
|
||||
data: null,
|
||||
url: null
|
||||
};
|
||||
|
||||
$scope.developerModeChange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
password: ''
|
||||
};
|
||||
|
||||
$scope.createBackup = {
|
||||
busy: false
|
||||
};
|
||||
|
||||
$scope.nameChange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
name: ''
|
||||
};
|
||||
|
||||
$scope.avatarChange = {
|
||||
busy: false,
|
||||
error: {},
|
||||
avatar: null,
|
||||
availableAvatars: [{
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/avatar_0.png',
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/cloudfacegreen.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/cloudfaceturquoise.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/cloudglassesgreen.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/cloudglassespink.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/cloudglassesturquoise.png'
|
||||
}, {
|
||||
file: null,
|
||||
data: null,
|
||||
url: '/img/avatars/cloudglassesyellow.png'
|
||||
}]
|
||||
};
|
||||
|
||||
$scope.setPreviewAvatar = function (avatar) {
|
||||
$scope.avatarChange.avatar = avatar;
|
||||
};
|
||||
|
||||
$scope.showCustomAvatarSelector = function () {
|
||||
$('#avatarFileInput').click();
|
||||
};
|
||||
|
||||
function nameChangeReset() {
|
||||
$scope.nameChange.error.name = null;
|
||||
$scope.nameChange.name = '';
|
||||
|
||||
$scope.nameChangeForm.$setPristine();
|
||||
$scope.nameChangeForm.$setUntouched();
|
||||
}
|
||||
|
||||
function avatarChangeReset() {
|
||||
$scope.avatarChange.error.avatar = null;
|
||||
$scope.avatarChange.avatar = null;
|
||||
$scope.avatarChange.busy = false;
|
||||
}
|
||||
|
||||
function fetchBackups() {
|
||||
Client.getBackups(function (error, backups) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.backups = backups;
|
||||
|
||||
if ($scope.backups.length > 0) {
|
||||
$scope.lastBackup = backups[0];
|
||||
} else {
|
||||
$scope.lastBackup = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function developerModeChangeReset () {
|
||||
$scope.developerModeChange.error.password = null;
|
||||
$scope.developerModeChange.password = '';
|
||||
|
||||
$scope.developerModeChangeForm.$setPristine();
|
||||
$scope.developerModeChangeForm.$setUntouched();
|
||||
}
|
||||
|
||||
$scope.doChangeDeveloperMode = function () {
|
||||
$scope.developerModeChange.error.password = null;
|
||||
$scope.developerModeChange.busy = true;
|
||||
|
||||
Client.changeDeveloperMode(!$scope.config.developerMode, $scope.developerModeChange.password, function (error) {
|
||||
if (error) {
|
||||
if (error.statusCode === 403) {
|
||||
$scope.developerModeChange.error.password = true;
|
||||
$scope.developerModeChange.password = '';
|
||||
$('#inputDeveloperModeChangePassword').focus();
|
||||
} else {
|
||||
console.error('Unable to change developer mode.', error);
|
||||
}
|
||||
} else {
|
||||
developerModeChangeReset();
|
||||
|
||||
$('#developerModeChangeModal').modal('hide');
|
||||
}
|
||||
|
||||
$scope.developerModeChange.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doChangeName = function () {
|
||||
$scope.nameChange.error.name = null;
|
||||
$scope.nameChange.busy = true;
|
||||
|
||||
Client.changeCloudronName($scope.nameChange.name, function (error) {
|
||||
if (error) {
|
||||
console.error('Unable to change name.', error);
|
||||
} else {
|
||||
nameChangeReset();
|
||||
$('#nameChangeModal').modal('hide');
|
||||
}
|
||||
|
||||
$scope.nameChange.busy = false;
|
||||
});
|
||||
};
|
||||
|
||||
function getBlobFromImg(img, callback) {
|
||||
var size = 256;
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
|
||||
var imageDimensionRatio = img.width / img.height;
|
||||
var canvasDimensionRatio = canvas.width / canvas.height;
|
||||
var renderableHeight, renderableWidth, xStart, yStart;
|
||||
|
||||
if (imageDimensionRatio > canvasDimensionRatio) {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = img.width * (renderableHeight / img.height);
|
||||
xStart = (canvas.width - renderableWidth) / 2;
|
||||
yStart = 0;
|
||||
} else if (imageDimensionRatio < canvasDimensionRatio) {
|
||||
renderableWidth = canvas.width;
|
||||
renderableHeight = img.height * (renderableWidth / img.width);
|
||||
xStart = 0;
|
||||
yStart = (canvas.height - renderableHeight) / 2;
|
||||
} else {
|
||||
renderableHeight = canvas.height;
|
||||
renderableWidth = canvas.width;
|
||||
xStart = 0;
|
||||
yStart = 0;
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, xStart, yStart, renderableWidth, renderableHeight);
|
||||
|
||||
canvas.toBlob(callback);
|
||||
}
|
||||
|
||||
$scope.doChangeAvatar = function () {
|
||||
$scope.avatarChange.error.avatar = null;
|
||||
$scope.avatarChange.busy = true;
|
||||
|
||||
var img = document.getElementById('previewAvatar');
|
||||
$scope.avatarChange.avatar.file = getBlobFromImg(img, function (blob) {
|
||||
Client.changeCloudronAvatar(blob, function (error) {
|
||||
if (error) {
|
||||
console.error('Unable to change developer mode.', error);
|
||||
} else {
|
||||
$scope.avatar = $scope.avatarChange.avatar;
|
||||
avatarChangeReset();
|
||||
$('#avatarChangeModal').modal('hide');
|
||||
}
|
||||
|
||||
$scope.avatarChange.busy = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doCreateBackup = function () {
|
||||
$('#createBackupModal').modal('hide');
|
||||
$scope.createBackup.busy = true;
|
||||
$scope.createBackup.percent = 100;
|
||||
|
||||
Client.backup(function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
$scope.createBackup.busy = false;
|
||||
}
|
||||
|
||||
function checkIfDone() {
|
||||
Client.progress(function (error, data) {
|
||||
if (error) return window.setTimeout(checkIfDone, 250);
|
||||
|
||||
// check if we are done
|
||||
if (!data.backup || data.backup.percent >= 100) {
|
||||
fetchBackups();
|
||||
$scope.createBackup.busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.createBackup.percent = data.backup.percent;
|
||||
window.setTimeout(checkIfDone, 250);
|
||||
});
|
||||
}
|
||||
|
||||
checkIfDone();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showChangeDeveloperMode = function () {
|
||||
developerModeChangeReset();
|
||||
$('#developerModeChangeModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.showChangeName = function () {
|
||||
nameChangeReset();
|
||||
$('#nameChangeModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.showCreateBackup = function () {
|
||||
$('#createBackupModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.showChangeAvatar = function () {
|
||||
avatarChangeReset();
|
||||
$('#avatarChangeModal').modal('show');
|
||||
};
|
||||
|
||||
$('#avatarFileInput').get(0).onchange = function (event) {
|
||||
var fr = new FileReader();
|
||||
fr.onload = function () {
|
||||
$scope.$apply(function () {
|
||||
var tmp = {
|
||||
file: event.target.files[0],
|
||||
data: fr.result,
|
||||
url: null
|
||||
};
|
||||
|
||||
$scope.avatarChange.availableAvatars.push(tmp);
|
||||
$scope.setPreviewAvatar(tmp);
|
||||
});
|
||||
};
|
||||
fr.readAsDataURL(event.target.files[0]);
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
fetchBackups();
|
||||
|
||||
$scope.avatar.url = '//my-' + $scope.config.fqdn + '/api/v1/cloudron/avatar';
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['developerModeChangeModal', 'nameChangeModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
8
webadmin/src/views/setup/step1.html
Normal file
8
webadmin/src/views/setup/step1.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<center>
|
||||
<h1>Welcome to your Cloudron</h1>
|
||||
<br/>
|
||||
<h3 class="">This is your <b>{{ wizard.hostname }}</b> and all your apps will be installed under this domain.</h3>
|
||||
<br/>
|
||||
<br/>
|
||||
<a class="btn btn-primary" href="#/step2" autofocus>Forward</a>
|
||||
</center>
|
||||
43
webadmin/src/views/setup/step2.html
Normal file
43
webadmin/src/views/setup/step2.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Personalize your Cloudron</h1>
|
||||
<h4 class="">
|
||||
Make it truly yours, by giving your Cloudron an avatar and name.
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 settings-avatar-selector">
|
||||
<img id="previewAvatar" width="128" height="128" ng-src="{{wizard.avatar.data || wizard.avatar.url}}"/>
|
||||
<input type="file" id="avatarFileInput" style="display: none" accept="image/png"/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="grid">
|
||||
<div class="item" ng-repeat="avatar in wizard.availableAvatars" style="background-image: url('{{avatar.data || avatar.url}}');" ng-click="wizard.setPreviewAvatar(avatar)"></div>
|
||||
<div class="item add" ng-click="showCustomAvatarSelector()"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4 text-center">
|
||||
<div class="form-group" ng-class="{ 'has-error': setup_form.name.$dirty && setup_form.name.$invalid }">
|
||||
<!-- <label class="control-label" for="inputName">Name</label> -->
|
||||
<input type="text" class="form-control" ng-model="wizard.name" id="inputName" name="name" placeholder="Name" ng-enter="next('/step3', setup_form.name.$invalid)" ng-maxlength="512" ng-minlength="1" autofocus required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<a class="btn btn-primary" href="#/step3" ng-disabled="setup_form.name.$invalid">Next</a>
|
||||
</div>
|
||||
</div>
|
||||
26
webadmin/src/views/setup/step3.html
Normal file
26
webadmin/src/views/setup/step3.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<h1>Create an Administrator for <b>{{ wizard.name }}</b>, your Cloudron</h1>
|
||||
<h4 class="">
|
||||
You can create more users once we are done here.
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-4 text-center">
|
||||
<div class="form-group" ng-class="{ 'has-error': setup_form.username.$dirty && setup_form.username.$invalid }">
|
||||
<!-- <label class="control-label" for="inputUsername">Username</label> -->
|
||||
<input type="text" class="form-control" ng-model="wizard.username" id="inputUsername" name="username" placeholder="Username" ng-enter="focusNext('inputPassword', setup_form.username.$invalid)" ng-maxlength="512" ng-minlength="3" autofocus required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }">
|
||||
<!-- <label class="control-label" for="inputPassword">Password</label> -->
|
||||
<input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next('/step4', setup_form.password.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<a class="btn btn-primary" href="#/step4" ng-disabled="setup_form.username.$invalid">Done</a>
|
||||
</div>
|
||||
</div>
|
||||
8
webadmin/src/views/setup/step4.html
Normal file
8
webadmin/src/views/setup/step4.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<center>
|
||||
<h1>All done!</h1>
|
||||
<br/>
|
||||
<br/>
|
||||
<i class="fa fa-spinner fa-pulse fa-5x"></i>
|
||||
<br/>
|
||||
<br/>
|
||||
</center>
|
||||
135
webadmin/src/views/upgrade.html
Normal file
135
webadmin/src/views/upgrade.html
Normal file
@@ -0,0 +1,135 @@
|
||||
<!-- Modal upgrade -->
|
||||
<div class="modal fade" id="upgradeModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Upgrade your Cloudron</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Do you really want to upgrade your Cloudron?<br/>
|
||||
<br/>
|
||||
<span class="text-danger">Your Cloudron will have a downtime of around 10 minutes, where none of you apps will be reachable.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<form name="upgradeForm" class="form-signin" role="form" novalidate ng-submit="upgrade()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (upgradeForm.password.$dirty && upgradeForm.password.$invalid) || (!upgradeForm.password.$dirty && upgrade.error.password) }">
|
||||
<label class="control-label" for="upgradePasswordInput">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="upgrade.password" id="upgradePasswordInput" name="password" ng-maxlength="512" ng-minlength="5" required autofocus autocomplete="off">
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="upgradeForm.$invalid || busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="upgrade()" ng-disabled="upgradeForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="busy"></i> Upgrade</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal relocate -->
|
||||
<div class="modal fade" id="relocationModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Relocate your Cloudron</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Do you really want to relocate your Cloudron from <b>{{currentRegionSlug}}</b> to <b>{{relocation.region.slug}}</b><br/>
|
||||
<br/>
|
||||
<span class="text-danger">Your Cloudron will have a downtime of around 10 minutes, where none of you apps will be reachable.</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<form name="relocateForm" class="form-signin" role="form" novalidate ng-submit="relocate()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (relocateForm.password.$dirty && relocateForm.password.$invalid) || (!relocateForm.password.$dirty && relocation.error.password) }">
|
||||
<label class="control-label" for="relocationPasswordInput">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="relocation.password" id="relocationPasswordInput" name="password" ng-maxlength="512" ng-minlength="5" required autofocus autocomplete="off">
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="relocateForm.$invalid || busy"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="relocate()" ng-disabled="relocateForm.$invalid || busy"><i class="fa fa-spinner fa-pulse" ng-show="busy"></i> Relocate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div style="margin-bottom: 15px; text-align: center" ng-show="user.admin">
|
||||
<div class="row" ng-show="availableSizes.length > 1">
|
||||
<div class="col-md-12">
|
||||
<h3>Choose a Model to upgrade to</h3>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row" ng-show="availableSizes.length > 1">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="cloudron-model-list">
|
||||
<div class="cloudron-model-item" ng-repeat="size in availableSizes">
|
||||
<div class="cloudron-model-item-content shadow" ng-class="{ 'selected': size.slug === currentSize.slug }" style="height: {{ 120 + $index * 30 }}px">
|
||||
<!-- <img src="img/box.png" style="transform: scale({{ size.price/50.0 }});"/><br/> -->
|
||||
<h3>{{ size.name }}</h3>
|
||||
<h5>${{ size.price }}/mo</h5>
|
||||
<button class="btn btn-success" ng-disabled="busy" ng-hide="size.slug === currentSize.slug" ng-click="showUpgradeConfirm(size)">Upgrade</button>
|
||||
<button class="btn btn-success" ng-show="size.slug === currentSize.slug" data-toggle="tooltip" data-placement="top" title="Your Current Model"><i class="fa fa-check"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="availableSizes.length > 1">
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
Larger Cloudrons allow to run more apps and bring better performance to your installed services.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="availableSizes.length <= 1">
|
||||
<div class="col-md-12">
|
||||
<h4>You are already using the largest available Cloudron model.</h4>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-12">
|
||||
<h3>Choose your Region, in case you want to relocate your Cloudron</h3>
|
||||
<p>
|
||||
The closer you are to your Cloudron, the faster the access speed will be.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-12">
|
||||
<div class="region-select-map">
|
||||
<div class="region-select" ng-click="setRegion('sfo1')" ng-class="{ 'region-selected': relocation.region.slug === 'sfo1' }" style="width: 270px; background-image: url('img/world_left.png');">
|
||||
<div class="region-pin region-pin-left" ng-class="{ 'region-pin-selected-left': relocation.region.slug === 'sfo1' }">
|
||||
<img src="img/pin.png" height="32px" width="16px"/> San Francisco
|
||||
</div>
|
||||
</div>
|
||||
<div class="region-select" ng-click="setRegion('ams3')" ng-class="{ 'region-selected': relocation.region.slug === 'ams3' }" style="width: 330px; background-image: url('img/world_right.png');">
|
||||
<div class="region-pin region-pin-right" ng-class="{ 'region-pin-selected-right': relocation.region.slug === 'ams3' }">
|
||||
<img src="img/pin.png" height="32px" width="16px"/> Amsterdam
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-md-12">
|
||||
<button class="btn btn-success" ng-click="showRelocationConfirm()" ng-disabled="(relocation.region.slug === currentRegionSlug) || busy">Relocate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
114
webadmin/src/views/upgrade.js
Normal file
114
webadmin/src/views/upgrade.js
Normal file
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('UpgradeController', ['$scope', '$location', 'Client', 'AppStore', function ($scope, $location, Client, AppStore) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.busy = false;
|
||||
$scope.availableRegions = [];
|
||||
$scope.availableSizes = [];
|
||||
|
||||
$scope.currentSize = null;
|
||||
$scope.currentRegionSlug = null;
|
||||
|
||||
$scope.upgrade = {
|
||||
size: null,
|
||||
error: {},
|
||||
password: null
|
||||
};
|
||||
|
||||
$scope.relocation = {
|
||||
region: null,
|
||||
error: {},
|
||||
password: null
|
||||
};
|
||||
|
||||
$scope.showUpgradeConfirm = function (size) {
|
||||
$scope.upgrade.size = size;
|
||||
$('#upgradeModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.upgrade = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
Client.migrate($scope.upgrade.size.slug, $scope.currentRegionSlug, $scope.upgrade.password, function (error) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.upgrade.error.password = true;
|
||||
$scope.upgrade.password = '';
|
||||
$('#upgradePasswordInput').focus();
|
||||
return;
|
||||
} else if (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
$('#upgradeModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showRelocationConfirm = function () {
|
||||
$('#relocationModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.relocate = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
Client.migrate($scope.currentSize.slug, $scope.relocation.region.slug, $scope.relocation.password, function (error) {
|
||||
$scope.busy = false;
|
||||
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.relocation.error.password = true;
|
||||
$scope.relocation.password = '';
|
||||
$('#relocationPasswordInput').focus();
|
||||
return;
|
||||
} else if (error) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
$('#relocationModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setRegion = function (regionSlug) {
|
||||
$scope.availableRegions.forEach(function (region) {
|
||||
if (region.slug.indexOf(regionSlug) === 0) $scope.relocation.region = region;
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
AppStore.getSizes(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
// result array is ordered by size
|
||||
var found = false;
|
||||
result = result.filter(function (size) {
|
||||
if (size.slug === $scope.config.size) {
|
||||
$scope.currentSize = size;
|
||||
found = true;
|
||||
return true;
|
||||
} else {
|
||||
return found;
|
||||
}
|
||||
});
|
||||
angular.copy(result, $scope.availableSizes);
|
||||
|
||||
AppStore.getRegions(function (error, result) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
angular.copy(result, $scope.availableRegions);
|
||||
|
||||
$scope.currentRegionSlug = $scope.config.region;
|
||||
$scope.setRegion($scope.config.region);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['upgradeModal', 'relocationModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
130
webadmin/src/views/users.html
Normal file
130
webadmin/src/views/users.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<!-- Modal add user -->
|
||||
<div class="modal fade" id="userAddModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="userAddModalLabel">Add User</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="useradd_form" class="form-signin" role="form" novalidate ng-submit="doAdd()" autocomplete="off">
|
||||
<fieldset>
|
||||
<div class="form-group" ng-class="{ 'has-error': (useradd_form.username.$dirty && useradd_form.username.$invalid) || (!useradd_form.username.$dirty && useradd.error.username) }">
|
||||
<label class="control-label" for="inputUserAddUsername">Username</label>
|
||||
<div class="control-label" ng-show="(!useradd_form.username.$dirty && useradd.error.username) || (useradd_form.username.$dirty && useradd_form.username.$invalid) || (!useradd_form.username.$dirty && useradd.error.username)">
|
||||
<small ng-show="useradd_form.username.$error.required">A username is required</small>
|
||||
<small ng-show="useradd_form.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="useradd_form.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="!useradd_form.username.$dirty && useradd.error.username">{{ useradd.error.username }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="useradd.username" id="inputUserAddUsername" name="username" ng-maxlength="512" ng-minlength="3" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (useradd_form.email.$dirty && useradd_form.email.$invalid) || (!useradd_form.email.$dirty && useradd.error.email) }">
|
||||
<label class="control-label" for="inputUserAddEmail">Email</label>
|
||||
<div class="control-label" ng-show="(!useradd_form.email.$dirty && useradd.error.email) || (useradd_form.email.$dirty && useradd_form.email.$invalid) || (!useradd_form.email.$dirty && useradd.error.email)">
|
||||
<small ng-show="useradd_form.email.$error.required">An email is required</small>
|
||||
<small ng-show="useradd_form.email.$error.email">This is not a valid email</small>
|
||||
<small ng-show="!useradd_form.email.$dirty && useradd.error.email">{{ useradd.error.email }}</small>
|
||||
</div>
|
||||
<input type="email" class="form-control" ng-model="useradd.email" id="inputUserAddEmail" name="email" required>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="useradd_form.$invalid || useradd.alreadyTaken === username"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="doAdd()" ng-disabled="useradd_form.$invalid || useradd.busy"><i class="fa fa-spinner fa-pulse" ng-show="useradd.busy"></i> Add User</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal remove user -->
|
||||
<div class="modal fade" id="userRemoveModal" tabindex="-1" role="dialog" aria-labelledby="userRemoveModalLabel" aria-hidden="true" style="text-align: left;">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="userRemoveModalLabel">Delete user {{ userremove.userInfo.username }}</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<fieldset>
|
||||
<form name="userremove_form" class="form-user-delete" role="form" ng-submit="doUserRemove()" name="userDeleteConfirm" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (userremove_form.username.$dirty && userremove_form.username.$invalid) || (!userremove_form.username.$dirty && userremove.error.username) }">
|
||||
<label class="control-label" for="inputUserRemoveUsername">Just to be sure you really want to delete this user, please type the user's name</label>
|
||||
<div class="control-label" ng-show="(!userremove_form.username.$dirty && userremove.error.username) || (userremove_form.username.$dirty && userremove_form.username.$invalid)">
|
||||
<small ng-show="userremove_form.username.$error.required">A username is required</small>
|
||||
<small ng-show="userremove_form.error.username">The username does not match</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="userremove.username" id="inputUserRemoveUsername" name="userDeleteConfirm" placeholder="Username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (userremove_form.password.$dirty && userremove_form.password.$invalid) || (!userremove_form.password.$dirty && userremove.error.password)}">
|
||||
<label class="control-label" for="inputUserRemovePassword">Give your password to verify that you are performing that action</label>
|
||||
<div class="control-label" ng-show="(!userremove_form.password.$dirty && userremove.error.password) || (userremove_form.password.$dirty && userremove_form.password.$invalid)">
|
||||
<small ng-show="userremove_form.password.$error.required && !userremove.error.password">A password is required</small>
|
||||
<small ng-show="userremove_form.password.$error.minlength">The password is too short</small>
|
||||
<small ng-show="userremove_form.password.$error.maxlength">The password is too long</small>
|
||||
<small ng-show="!useradd_form.email.$dirty && userremove.error.password">{{ userremove.error.password }}</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="userremove.password" id="inputUserRemovePassword" name="password" placeholder="Password" ng-maxlength="512" ng-minlength="5" required>
|
||||
</div>
|
||||
<input class="hide" type="submit"/>
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doUserRemove()" ng-disabled="userremove_form.$invalid || userremove.busy"><i class="fa fa-spinner fa-pulse" ng-show="userremove.busy"></i> Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div>
|
||||
<div class="text-left">
|
||||
<h1>Users <button class="btn btn-primary btn-outline pull-right" data-toggle="modal" data-target="#userAddModal"><i class="fa fa-user-plus"></i> New User</button></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-large">
|
||||
<div class="grid-item-top">
|
||||
<div class="row ng-hide" ng-show="!ready">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2><i class="fa fa-spinner fa-pulse"></i></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="ready">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="">User</th>
|
||||
<th style="width: 1px" class="text-right">Group</th>
|
||||
<th style="width: 200px" class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="user in users">
|
||||
<td class="text-overflow: ellipsis; white-space: nowrap;">
|
||||
{{ user.username }}
|
||||
<span class="text-muted">{{ user.email }}</span>
|
||||
</td>
|
||||
<td class="text-right" style="vertical-align: bottom">
|
||||
<span ng-show="isAdmin(user)" class="label label-default">Admin</span>
|
||||
</td>
|
||||
<td class="text-right" style="vertical-align: bottom">
|
||||
<span ng-show="isMe(user)" class="label label-success">This is you!</span>
|
||||
<button ng-show="!isMe(user) && userInfo.admin" ng-click="toggleAdmin(user)" class="btn btn-xs btn-default">
|
||||
<span ng-show="!user.admin"><i class="fa fa-plus"></i> Add Admin</span>
|
||||
<span ng-show="user.admin"><i class="fa fa-minus"></i> Remove Admin</span>
|
||||
</button>
|
||||
<button ng-show="!isMe(user) && userInfo.admin" class="btn btn-xs btn-danger" ng-click="showUserRemove(user)"><i class="fa fa-trash-o"></i> Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
152
webadmin/src/views/users.js
Normal file
152
webadmin/src/views/users.js
Normal file
@@ -0,0 +1,152 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('Application').controller('UsersController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
|
||||
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
|
||||
|
||||
$scope.ready = false;
|
||||
$scope.users = [];
|
||||
$scope.userInfo = Client.getUserInfo();
|
||||
|
||||
$scope.userremove = {
|
||||
busy: false,
|
||||
error: {},
|
||||
userInfo: {},
|
||||
username: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
$scope.useradd = {
|
||||
busy: false,
|
||||
alreadyTaken: false,
|
||||
error: {},
|
||||
username: '',
|
||||
email: ''
|
||||
};
|
||||
|
||||
$scope.isMe = function (user) {
|
||||
return user.username === Client.getUserInfo().username;
|
||||
};
|
||||
|
||||
$scope.isAdmin = function (user) {
|
||||
return !!user.admin;
|
||||
};
|
||||
|
||||
$scope.toggleAdmin = function (user) {
|
||||
Client.setAdmin(user.username, !user.admin, function (error) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
user.admin = !user.admin;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.doAdd = function () {
|
||||
$scope.useradd.busy = true;
|
||||
|
||||
$scope.useradd.alreadyTaken = false;
|
||||
$scope.useradd.error.username = null;
|
||||
$scope.useradd.error.email = null;
|
||||
|
||||
Client.createUser($scope.useradd.username, $scope.useradd.email, function (error) {
|
||||
$scope.useradd.busy = false;
|
||||
|
||||
if (error && error.statusCode === 409) {
|
||||
$scope.useradd.error.username = 'Username or Email already taken';
|
||||
$scope.useradd_form.username.$setPristine();
|
||||
$scope.useradd_form.email.$setPristine();
|
||||
$('#inputUserAddUsername').focus();
|
||||
return;
|
||||
}
|
||||
if (error && error.statusCode === 400) {
|
||||
if (error.message.indexOf('email') !== -1) {
|
||||
$scope.useradd.error.email = 'Invalid Email';
|
||||
$scope.useradd.error.emailAttempted = $scope.useradd.email;
|
||||
$scope.useradd_form.email.$setPristine();
|
||||
$('#inputUserAddEmail').focus();
|
||||
} else if (error.message.indexOf('username') !== -1) {
|
||||
$scope.useradd.error.username = 'Invalid Username';
|
||||
$scope.useradd.error.usernameAttempted = $scope.useradd.username;
|
||||
$scope.useradd_form.username.$setPristine();
|
||||
$('#inputUserAddUsername').focus();
|
||||
} else {
|
||||
console.error('Unable to create user.', error.statusCode, error.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (error) return console.error('Unable to create user.', error.statusCode, error.message);
|
||||
|
||||
$scope.useradd.error = {};
|
||||
$scope.useradd.username = '';
|
||||
$scope.useradd.email = '';
|
||||
|
||||
$scope.useradd_form.$setUntouched();
|
||||
$scope.useradd_form.$setPristine();
|
||||
|
||||
refresh();
|
||||
|
||||
$('#userAddModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showUserRemove = function (userInfo) {
|
||||
$scope.userremove.error.username = null;
|
||||
$scope.userremove.error.password = null;
|
||||
$scope.userremove.userInfo = userInfo;
|
||||
$('#userRemoveModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.doUserRemove = function () {
|
||||
$scope.userremove.error.username = null;
|
||||
$scope.userremove.error.password = null;
|
||||
|
||||
if ($scope.userremove.username !== $scope.userremove.userInfo.username) {
|
||||
$scope.userremove.error.username = 'Username does not match';
|
||||
$scope.userremove.username = '';
|
||||
$('#inputUserRemoveUsername').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.userremove.busy = true;
|
||||
|
||||
Client.removeUser($scope.userremove.userInfo.id, $scope.userremove.password, function (error) {
|
||||
$scope.userremove.busy = false;
|
||||
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.userremove.error.password = 'Incorrect password';
|
||||
$scope.userremove.password = '';
|
||||
$scope.userremove_form.password.$setPristine();
|
||||
$('#inputUserRemovePassword').focus();
|
||||
return;
|
||||
}
|
||||
if (error) return console.error('Unable to delete user.', error);
|
||||
|
||||
$scope.userremove.userInfo = {};
|
||||
$scope.userremove.username = '';
|
||||
$scope.userremove.password = '';
|
||||
|
||||
$scope.userremove_form.$setPristine();
|
||||
$scope.userremove_form.$setUntouched();
|
||||
|
||||
refresh();
|
||||
|
||||
$('#userRemoveModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
Client.listUsers(function (error, result) {
|
||||
if (error) return console.error('Unable to get user listing.', error);
|
||||
|
||||
$scope.users = result.users;
|
||||
$scope.ready = true;
|
||||
});
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['userAddModal', 'userRemoveModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
});
|
||||
}]);
|
||||
Reference in New Issue
Block a user