app.portBindings and newManifest.tcpPorts may be null

This commit is contained in:
Girish Ramakrishnan
2015-07-20 00:09:47 -07:00
commit df9d321ac3
243 changed files with 42623 additions and 0 deletions

View 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 }} &nbsp;&nbsp;</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>

View 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();
});
});
}]);

View 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
View 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();
});
});
}]);

View 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/>

View 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();
});
});
}]);

View 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
View 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);
});
};
}]);

View 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>
&nbsp;
<span class="text-warning">Reserved {{ diskUsage['docker'].reserved }} GB</span>
&nbsp;
<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>
&nbsp;
<span class="text-warning">Reserved {{ diskUsage['box'].reserved }} GB</span>
&nbsp;
<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>
&nbsp;
<span class="text-warning">Reserved {{ diskUsage['cloudron'].reserved }} GB</span>
&nbsp;
<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>
&nbsp;
</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>
&nbsp;
</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/>

View 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'));
}]);

View 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>

View 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();
});
});
}]);

View 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>

View 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>

View 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>

View 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>

View 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>

View 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();
});
});
}]);

View 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
View 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();
});
});
}]);