Move all app configure tasks to separate view

This commit is contained in:
Johannes Zellner
2019-09-10 19:21:30 +02:00
parent 8823656d70
commit a4c99fd361
6 changed files with 897 additions and 11 deletions

View File

@@ -460,12 +460,12 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
});
};
Client.prototype.configureApp = function (id, data, callback) {
post('/api/v1/apps/' + id + '/configure', data, null, function (error, data, status) {
Client.prototype.configureApp = function (id, setting, data, callback) {
post('/api/v1/apps/' + id + '/configure/' + setting, data, null, function (error, data, status) {
if (error) return callback(error);
if (status !== 202) return callback(new ClientError(status, data));
if (status !== 200 && status !== 202) return callback(new ClientError(status, data));
callback(null);
callback(null, data);
});
};
@@ -1027,12 +1027,13 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
};
Client.prototype.getApp = function (appId, callback) {
var that = this;
get('/api/v1/apps/' + appId, null, function (error, data, status) {
if (error) return callback(error);
if (status !== 200) return callback(new ClientError(status, data));
var tmp = data.manifest.description.match(/\<upstream\>(.*)\<\/upstream\>/i);
data.upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
that._appPostProcess(data);
callback(null, data);
});
@@ -1458,6 +1459,9 @@ angular.module('Application').service('Client', ['$http', '$interval', '$timeout
// amend the post install confirm state
app.pendingPostInstallConfirmation = !!localStorage['confirmPostInstall_' + app.id];
var tmp = app.manifest.description.match(/\<upstream\>(.*)\<\/upstream\>/i);
app.upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
return app;
};

View File

@@ -110,6 +110,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/users', {
controller: 'UsersController',
templateUrl: 'views/users.html?<%= revision %>'
}).when('/app/:appId', {
controller: 'AppController',
templateUrl: 'views/app.html?<%= revision %>'
}).when('/appstore', {
controller: 'AppStoreController',
templateUrl: 'views/appstore.html?<%= revision %>'

415
src/views/app.html Normal file
View File

@@ -0,0 +1,415 @@
<script>
function imageErrorHandler(elem) {
'use strict';
elem.src = elem.getAttribute('fallback-icon');
elem.onerror = null; // avoid retry after default icon cannot be loaded
}
</script>
<div class="content">
<div>
<h1>
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-icon"/>
{{ app.label || app.location || app.fqdn }}
</h1>
</div>
<div class="card">
<div class="row">
<div class="col-md-12">
<table width="100%">
<tr>
<td class="text-muted" style="vertical-align: top;">App ID</td>
<td class="text-right" style="vertical-align: top;">{{ app.id }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">Upstream Version</td>
<td class="text-right" style="vertical-align: top;">{{ app.upstreamVersion }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">App Package</td>
<td class="text-right" style="vertical-align: top;">{{ app.manifest.title }}</td>
</tr>
<tr>
<td class="text-muted" style="vertical-align: top;">App Package Version</td>
<td class="text-right" style="vertical-align: top;">{{ app.manifest.version }}</td>
</tr>
</table>
<br/>
<a class="btn btn-outline btn-primary pull-right" ng-href="{{ app.manifest.documentationUrl }}" target="_blank">Documentation</a>
</div>
</div>
</div>
<div><h3>Display</h3></div>
<div class="card">
<div class="row">
<div class="col-md-12">
<fieldset>
<form role="form" name="displayForm" ng-submit="display.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': !displayForm.label.$dirty && display.error.label }">
<label class="control-label">Label</label>
<div class="control-label" ng-show="display.error.label">{{display.error.label}}</div>
<input type="text" class="form-control" id="displayLabelInput" name="label" ng-model="display.label">
</div>
<div class="form-group">
<label class="control-label">Tags</label>
<tag-input class="form-control" placeholder="Use comma to separate tags" taglist="display.tags" name="tags" uib-tooltip="For grouping in the dashboard"></tag-input>
</div>
<div class="form-group">
<div>
<label class="control-label">Icon</label>
</div>
<div id="previewIcon" class="app-custom-icon" ng-click="display.showCustomIconSelector()" style="background-image: url('{{ display.iconUrl() }}');">
<div class="overlay"></div>
</div>
<a href="" style="font-weight: normal;" ng-click="display.resetCustomIcon()">Reset Icon</a>
<input type="file" id="iconFileInput" style="display: none" accept="image/png"/>
</div>
<input class="ng-hide" type="submit" ng-disabled="displayForm.$invalid || display.busy"/>
</form>
</fieldset>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="display.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="display.submit()" ng-disabled="display.$invalid || display.busy"><i class="fa fa-circle-notch fa-spin" ng-show="display.busy"></i> Save</button>
</div>
</div>
</div>
<div><h3>Location</h3></div>
<div class="card">
<div class="row">
<div class="col-md-12">
<fieldset>
<form role="form" name="locationForm" ng-submit="location.submit()" autocomplete="off">
<div class="has-error text-center" ng-show="location.error.other">{{ location.error.other }}</div>
<div class="form-group" ng-class="{ 'has-error': (locationForm.location.$dirty && locationForm.location.$invalid) || (!locationForm.location.$dirty && location.error.location) }">
<label class="control-label" for="locationLocationInput">Location {{ location.error.location }} </label>
<div class="input-group form-inline">
<input type="text" class="form-control" ng-model="location.location" id="locationLocationInput" name="location" placeholder="{{ 'Leave empty to use bare domain' }}" autofocus>
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>{{ (!location.location ? '' : (location.domain.config.hyphenatedSubdomains ? '-' : '.')) + location.domain.domain }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li ng-repeat="domain in domains">
<a href="" ng-click="location.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</div>
<p class="text-center" ng-show="location.location && location.domain.provider === 'manual'">
<b>Add an A record manually for {{ location.location }} to this Cloudron's public IP</b>
<br>
</p>
<div class="has-error text-center" ng-show="location.error.port">{{ location.error.port }}</div>
<div ng-repeat="(env, info) in location.portBindingsInfo">
<ng-form name="portInfo_form">
<div class="form-group" ng-class="{ 'has-error': (!locationForm.itemName{{$index}}.$dirty && location.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
<label class="control-label" for="locationPortInput{{env}}"><input type="checkbox" ng-model="location.portBindingsEnabled[env]">
{{ info.title }}
<sup>
<a popover-placement="top-right" popover-trigger="outsideClick" uib-popover="{{info.description}} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})"><i class="fa fa-question-circle"></i></a>
</sup>
</label>
<input type="number" class="form-control" ng-model="location.portBindings[env]" ng-disabled="!location.portBindingsEnabled[env]" id="locationPortInput{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
</div>
</ng-form>
</div>
<div class="form-group alternate-domains">
<label class="control-label">Redirections <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#redirections" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<div class="has-error" ng-show="location.error.alternateDomains">{{ location.error.alternateDomains }}</div>
<div class="row" ng-repeat="alternateDomain in location.alternateDomains">
<div class="col col-lg-11">
<div class="input-group">
<input type="text" class="form-control" ng-model="alternateDomain.subdomain" placeholder="Leave empty to use bare domain">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>{{ (!alternateDomain.subdomain ? '' : (alternateDomain.domain.config.hyphenatedSubdomains ? '-' : '.')) + alternateDomain.domain.domain }}</span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li ng-repeat="domain in domains">
<a href="" ng-click="alternateDomain.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</div>
<div class="col col-lg-1">
<button class="btn btn-danger btn-sm" ng-click="location.delAlternateDomain($event, $index)"><i class="far fa-trash-alt"></i></button>
</div>
</div>
<div ng-show="location.alternateDomains.length === 0">
No alternate domains are configured. <a href="" ng-click="location.addAlternateDomain($event)">Add a domain</a>
</div>
<div ng-show="location.alternateDomains.length > 0" style="margin-top: 5px;">
<a href="" ng-click="location.addAlternateDomain($event)">Add another domain</a>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="locationForm.$invalid || location.busy"/>
</form>
</fieldset>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="location.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="location.submit()" ng-disabled="location.$invalid || location.busy"><i class="fa fa-circle-notch fa-spin" ng-show="location.busy"></i> Save</button>
</div>
</div>
</div>
<div><h3>Access Control</h3></div>
<div class="card">
<div class="row">
<div class="col-md-12">
<fieldset>
<form role="form" name="accessForm" ng-submit="access.submit()" autocomplete="off">
<div class="form-group">
<div ng-show="access.ssoAuth">
<label class="control-label">User management</label>
<p class="text-small" ng-show="access.ftp">This setting also controls SFTP access.</p>
</div>
<div ng-show="!access.ssoAuth">
<label class="control-label">Dashboard visibility</label>
<p ng-show="!access.app.manifest.addons.email" class="text-small">
This app has it's own user management.
<span ng-show="access.ftp">This setting also controls SFTP access.</span>
</p>
<p ng-show="access.app.manifest.addons.email" class="text-small">
This app is pre-configured for use with <a href="https://cloudron.io/documentation/email/" target="_blank">Cloudron Email</a>.
</p>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="access.accessRestrictionOption" value="any">
<span ng-show="access.ssoAuth">Allow all users on this Cloudron</span>
<span ng-show="!access.ssoAuth">Visible to all users on this Cloudron</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="access.accessRestrictionOption" value="groups">
<span ng-show="access.ssoAuth">Only allow the following users and groups</span>
<span ng-show="!access.ssoAuth">Only visible to the following users and groups</span>
<span class="label label-danger" ng-show="access.accessRestrictionOption === 'groups' && !access.isAccessRestrictionValid()">Select at least one user or group</span>
</label>
</div>
<div>
<div style="margin-left: 20px;">
<div class="col-md-5">
Users: <multiselect class="input-sm stretch" ng-model="access.accessRestriction.users" ng-disabled="access.accessRestrictionOption !== 'groups'" options="user.display for user in users" data-multiple="true"></multiselect>
</div>
<div class="col-md-5">
Groups: <multiselect class="input-sm stretch" ng-model="access.accessRestriction.groups" ng-disabled="access.accessRestrictionOption !== 'groups'" options="group.name for group in groups" data-multiple="true"></multiselect>
</div>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="accessForm.$invalid || access.busy"/>
</div>
</form>
</fieldset>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="access.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="access.submit()" ng-disabled="access.$invalid || access.busy"><i class="fa fa-circle-notch fa-spin" ng-show="access.busy"></i> Save</button>
</div>
</div>
</div>
<div><h3>Resources</h3></div>
<div class="card">
<div class="row">
<div class="col-md-12">
<fieldset>
<form role="form" name="resourcesForm" ng-submit="resources.submit()" autocomplete="off">
<div class="form-group">
<label class="control-label" for="memoryLimit">Memory Limit <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#increasing-the-memory-limit-of-an-app" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ resources.memoryLimit ? resources.memoryLimit / 1024 / 1024 + 'MB' : 'Default (256 MB)' }}</b></label>
<br/>
<div style="padding: 0 10px;">
<slider id="memoryLimit" ng-model="resources.memoryLimit" step="134217728" tooltip="hide" ticks="resources.memoryTicks" ticks-snap-bounds="67108864"></slider>
</div>
</div>
<!-- We do not show this currently -->
<div ng-if="false" class="form-group" ng-class="{ 'has-error': !resourcesForm.dataDir.$dirty && resources.error.dataDir }">
<input type="checkbox" id="resourcesEnableDataDir" ng-model="resources.dataDirEnabled">
<label class="control-label" for="resourcesEnableDataDir">Custom Data Directory</label>
<div class="control-label" ng-show="resources.error.dataDir">{{resources.error.dataDir}}</div>
<input type="text" class="form-control" id="resourcesDataDirInput" name="dataDir" ng-disabled="!resources.dataDirEnabled" placeholder="/mnt/appdata" ng-model="resources.dataDir">
</div>
<input class="ng-hide" type="submit" ng-disabled="resourcesForm.$invalid || resources.busy"/>
</form>
</fieldset>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="resources.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="resources.submit()" ng-disabled="resources.$invalid || resources.busy"><i class="fa fa-circle-notch fa-spin" ng-show="resources.busy"></i> Save</button>
</div>
</div>
</div>
<div ng-if="app.manifest.addons.sendmail || app.manifest.addons.recvmail"><h3>Email</h3></div>
<div class="card" ng-if="app.manifest.addons.sendmail || app.manifest.addons.recvmail">
<div class="row">
<div class="col-md-12">
<fieldset>
<form role="form" name="emailForm" ng-submit="email.submit()" autocomplete="off">
<!-- recvmail currently only works with cloudron email -->
<div class="form-group" ng-class="{ 'has-error': !emailForm.mailboxName.$dirty && email.error.mailboxName }">
<input type="checkbox" id="emailMailboxNameEnabled" ng-model="email.mailboxNameEnabled">
<label class="control-label" for="emailMailboxNameEnabled">Custom Mail FROM</label>
<div class="has-error" ng-show="email.error.mailboxName">{{ email.error.mailboxName }}</div>
<div class="input-group form-inline">
<input type="text" class="form-control" ng-required="email.mailboxNameEnabled" name="mailboxName" ng-model="email.mailboxName" uib-tooltip="App FROM email address. Addresses ending with '.app' are reserved." tooltip-class="long" ng-disabled="!email.mailboxNameEnabled">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" ng-disabled="!email.mailboxNameEnabled">
@{{ email.domain.domain }}
</button>
</div>
</div>
<p class="text-small">
<br/>
This app is configured to send mail using <a ng-href="/#/email/{{ app.domain }}">{{app.domain}}'s Outbound Email</a> settings.
</p>
</div>
<input class="ng-hide" type="submit" ng-disabled="emailForm.$invalid || email.busy"/>
</form>
</fieldset>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="email.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="email.submit()" ng-disabled="email.$invalid || email.busy"><i class="fa fa-circle-notch fa-spin" ng-show="email.busy"></i> Save</button>
</div>
</div>
</div>
<div><h3>Security</h3></div>
<div class="card">
<div class="row">
<div class="col-md-12">
<fieldset>
<form role="form" name="securityForm" ng-submit="security.submit()" autocomplete="off">
<div class="form-group">
<label class="control-label" style="width: 100%">Specify robots.txt file content <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#indexing-by-search-engines-robotstxt" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> <a href="" class="pull-right" style="font-weight: normal;" ng-click="security.robotsTxt = disableIndexingTemplate">Disable indexing</a></label>
<textarea ng-model="security.robotsTxt" placeholder="Leave empty to allow all bots to index this app." class="form-control" rows="4"></textarea>
</div>
<input class="ng-hide" type="submit" ng-disabled="securityForm.$invalid || security.busy"/>
</form>
</fieldset>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="security.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="security.submit()" ng-disabled="security.$invalid || security.busy"><i class="fa fa-circle-notch fa-spin" ng-show="security.busy"></i> Save</button>
</div>
</div>
</div>
<div><h3>Updates</h3></div>
<div class="card">
<div class="row">
<div class="col-md-12">
<fieldset>
<form role="form" name="updatesForm" ng-submit="updates.submit()" autocomplete="off">
<div class="form-group">
<input type="checkbox" id="updatesEnableAutomaticUpdate" ng-model="updates.enableAutomaticUpdate">
<label class="control-label" for="updatesEnableAutomaticUpdate">Enable automatic updates</label>
</div>
<input class="ng-hide" type="submit" ng-disabled="updatesForm.$invalid || updates.busy"/>
</form>
</fieldset>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="updates.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="updates.submit()" ng-disabled="updates.$invalid || updates.busy"><i class="fa fa-circle-notch fa-spin" ng-show="updates.busy"></i> Save</button>
</div>
</div>
</div>
<div><h3>Backups</h3></div>
<div class="card">
<div class="row">
<div class="col-md-12">
<fieldset>
<form role="form" name="backupsForm" ng-submit="backups.submit()" autocomplete="off">
<div class="form-group">
<input type="checkbox" id="backupsEnableBackup" ng-model="backups.enableBackup">
<label class="control-label" for="backupsEnableBackup">Enable automatic daily backups</label>
</div>
<input class="ng-hide" type="submit" ng-disabled="backupsForm.$invalid || backups.busy"/>
</form>
</fieldset>
</div>
</div>
<div class="row">
<div class="col-md-6">
<span class="text-success text-bold" ng-show="backups.success">Saved</span>
</div>
<div class="col-md-6 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="backups.submit()" ng-disabled="backupsForm.$invalid || backups.busy"><i class="fa fa-circle-notch fa-spin" ng-show="backups.busy"></i> Save</button>
</div>
</div>
</div>
</div>

464
src/views/app.js Normal file
View File

@@ -0,0 +1,464 @@
'use strict';
/* global angular */
/* global $ */
/* global asyncSeries */
angular.module('Application').controller('AppController', ['$scope', '$location', '$timeout', '$interval', 'Client', function ($scope, $location, $timeout, $interval, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
var appId = $location.path().slice('/app/'.length);
$scope.app = null;
$scope.ready = false;
$scope.HOST_PORT_MIN = 1024;
$scope.HOST_PORT_MAX = 65535;
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.domains = [];
$scope.groups = [];
$scope.users = [];
$scope.backupsEnabled = true;
$scope.disableIndexingTemplate = '# Disable search engine indexing\n\nUser-agent: *\nDisallow: /';
$scope.display = {
busy: false,
error: {},
success: false,
tags: '',
label: '',
icon: { data: null },
iconUrl: function () {
if (!$scope.app) return '';
if ($scope.display.icon.data === '__original__') { // user clicked reset
return $scope.app.iconUrl + '&original=true';
} else if ($scope.display.icon.data) { // user uploaded icon
return $scope.display.icon.data;
} else { // current icon
return $scope.app.iconUrl;
}
},
resetCustomIcon: function () {
$scope.display.icon.data = '__original__';
},
showCustomIconSelector: function () {
$('#iconFileInput').click();
},
show: function () {
var app = $scope.app;
// translate for tag-input
$scope.display.tags = app.tags ? app.tags.join(',') : '';
$scope.display.label = $scope.app.label || '';
$scope.display.icon = { data: null };
$('#iconFileInput').get(0).onchange = function (event) {
var fr = new FileReader();
fr.onload = function () {
$scope.$apply(function () {
// var file = event.target.files[0];
$scope.display.icon.data = fr.result;
});
};
fr.readAsDataURL(event.target.files[0]);
};
},
submit: function () {
$scope.display.busy = true;
$scope.display.error = {};
// TODO break those apart
Client.configureApp($scope.app.id, 'label', { label: $scope.display.label }, function (error) {
if (error) return Client.error(error);
var tags = $scope.display.tags.split(',').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; });
Client.configureApp($scope.app.id, 'tags', { tags: tags }, function (error) {
if (error) return Client.error(error);
// skip if icon is unchanged
if ($scope.display.icon.data === null) {
$scope.display.busy = false;
$scope.display.success = true;
return;
}
var icon;
if ($scope.display.icon.data === '__original__') { // user reset the icon
icon = '';
} else if ($scope.display.icon.data) { // user loaded custom icon
icon = $scope.display.icon.data.replace(/^data:image\/[a-z]+;base64,/, '');
}
Client.configureApp($scope.app.id, 'icon', { icon: icon }, function (error) {
if (error) return Client.error(error);
$scope.display.busy = false;
$scope.display.success = true;
});
});
});
}
};
$scope.location = {
busy: false,
error: {},
success: false,
domain: null,
location: '',
alternateDomains: [],
portBindings: {},
portBindingsEnabled: {},
portBindingsInfo: {},
addAlternateDomain: function (event) {
event.preventDefault();
$scope.location.alternateDomains.push({
domain: $scope.domains[0],
subdomain: ''
});
},
delAlternateDomain: function (event, index) {
event.preventDefault();
$scope.location.alternateDomains.splice(index, 1);
},
show: function () {
var app = $scope.app;
$scope.location.location = app.location;
$scope.location.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
$scope.location.portBindingsInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
$scope.location.alternateDomains = app.alternateDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };});
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
for (var env in $scope.location.portBindingsInfo) {
if (app.portBindings && app.portBindings[env]) {
$scope.location.portBindings[env] = app.portBindings[env];
$scope.location.portBindingsEnabled[env] = true;
} else {
$scope.location.portBindings[env] = $scope.location.portBindingsInfo[env].defaultValue || 0;
$scope.location.portBindingsEnabled[env] = false;
}
}
},
submit: function () {
$scope.location.busy = true;
$scope.location.error = {};
// only use enabled ports from portBindings
var portBindings = {};
for (var env in $scope.location.portBindings) {
if ($scope.location.portBindingsEnabled[env]) {
portBindings[env] = $scope.location.portBindings[env];
}
}
var data = {
location: $scope.location.location,
domain: $scope.location.domain.domain,
portBindings: portBindings,
alternateDomains: $scope.location.alternateDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };})
};
Client.configureApp($scope.app.id, 'location', data, function (error) {
if (error) return Client.error(error);
$scope.location.success = true;
$scope.location.busy = false;
});
}
};
$scope.access = {
busy: false,
error: {},
success: false,
ftp: false,
ssoAuth: false,
accessRestrictionOption: 'any',
accessRestriction: { users: [], groups: [] },
isAccessRestrictionValid: function () {
var tmp = $scope.access.accessRestriction;
return !!(tmp.users.length || tmp.groups.length);
},
show: function () {
var app = $scope.app;
$scope.access.ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
$scope.access.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oauth']) && app.sso;
$scope.access.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
$scope.access.accessRestriction = { users: [], groups: [] };
if (app.accessRestriction) {
var userSet = { };
app.accessRestriction.users.forEach(function (uid) { userSet[uid] = true; });
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.access.accessRestriction.users.push(u); });
var groupSet = { };
app.accessRestriction.groups.forEach(function (gid) { groupSet[gid] = true; });
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.access.accessRestriction.groups.push(g); });
}
},
submit: function () {
$scope.access.busy = true;
$scope.access.error = {};
var accessRestriction = null;
if ($scope.access.accessRestrictionOption === 'groups') {
accessRestriction = { users: [], groups: [] };
accessRestriction.users = $scope.access.accessRestriction.users.map(function (u) { return u.id; });
accessRestriction.groups = $scope.access.accessRestriction.groups.map(function (g) { return g.id; });
}
Client.configureApp($scope.app.id, 'access_restriction', { accessRestriction: accessRestriction }, function (error) {
if (error) return Client.error(error);
$scope.access.success = true;
$scope.access.busy = false;
});
}
};
$scope.resources = {
busy: false,
error: {},
success: false,
memoryLimit: 0,
memoryTicks: [],
dataDir: null,
dataDirEnabled: false,
show: function () {
var app = $scope.app;
$scope.resources.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
$scope.resources.dataDirEnabled = !!app.dataDir;
$scope.resources.dataDir = app.dataDir;
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
$scope.resources.memoryTicks = [];
var npow2 = Math.pow(2, Math.ceil(Math.log($scope.config.memory)/Math.log(2)));
for (var i = 256; i <= (npow2*2/1024/1024); i *= 2) {
if (i >= (app.manifest.memoryLimit/1024/1024 || 0)) $scope.resources.memoryTicks.push(i * 1024 * 1024);
}
if (app.manifest.memoryLimit && $scope.resources.memoryTicks[0] !== app.manifest.memoryLimit) {
$scope.resources.memoryTicks.unshift(app.manifest.memoryLimit);
}
},
submit: function () {
$scope.resources.busy = true;
$scope.resources.error = {};
var memoryLimit = $scope.resources.memoryLimit === $scope.resources.memoryTicks[0] ? 0 : $scope.resources.memoryLimit;
Client.configureApp($scope.app.id, 'memory_limit', { memoryLimit: memoryLimit }, function (error) {
if (error) return Client.error(error);
$scope.resources.success = true;
$scope.resources.busy = false;
// TODO handle data dir once we show it
});
}
};
$scope.email = {
busy: false,
error: {},
success: false,
mailboxNameEnabled: false,
mailboxName: '',
domain: '',
show: function () {
var app = $scope.app;
$scope.email.mailboxNameEnabled = app.mailboxName && (app.mailboxName.match(/\.app$/) === null);
$scope.email.mailboxName = app.mailboxName || '';
$scope.email.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
},
submit: function () {
$scope.email.busy = true;
}
};
$scope.security = {
busy: false,
error: {},
success: false,
robotsTxt: '',
show: function () {
var app = $scope.app;
$scope.security.robotsTxt = app.robotsTxt;
},
submit: function () {
$scope.security.busy = true;
$scope.security.error = {};
Client.configureApp($scope.app.id, 'robots_txt', { robotsTxt: $scope.security.robotsTxt }, function (error) {
if (error) return Client.error(error);
$scope.security.success = true;
$scope.security.busy = false;
});
}
};
$scope.updates = {
busy: false,
error: {},
success: false,
enableAutomaticUpdate: false,
show: function () {
var app = $scope.app;
$scope.updates.enableAutomaticUpdate = app.enableAutomaticUpdate;
},
submit: function () {
$scope.updates.busy = true;
$scope.updates.error = {};
Client.configureApp($scope.app.id, 'automatic_update', { enable: $scope.updates.enableAutomaticUpdate }, function (error) {
if (error) return Client.error(error);
$scope.updates.success = true;
$scope.updates.busy = false;
});
}
};
$scope.backups = {
busy: false,
error: {},
success: false,
enableBackup: false,
show: function () {
var app = $scope.app;
$scope.backups.enableBackup = app.enableBackup;
},
submit: function () {
$scope.backups.busy = true;
$scope.backups.error = {};
Client.configureApp($scope.app.id, 'automatic_backup', { enable: $scope.backups.enableBackup }, function (error) {
if (error) return Client.error(error);
$scope.backups.success = true;
$scope.backups.busy = false;
});
}
};
function fetchUsers(callback) {
Client.getUsers(function (error, users) {
if (error) return callback(error);
// ensure we have something to work with in the access restriction dropdowns
users.forEach(function (user) { user.display = user.username || user.email; });
$scope.users = users;
callback();
});
}
function fetchGroups(callback) {
Client.getGroups(function (error, groups) {
if (error) return callback(error);
$scope.groups = groups;
callback();
});
}
function getDomains(callback) {
Client.getDomains(function (error, result) {
if (error) return callback(error);
$scope.domains = result;
callback();
});
}
function getBackupConfig(callback) {
Client.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
$scope.backupEnabled = backupConfig.provider !== 'noop';
callback();
});
}
Client.onReady(function () {
$scope.app = Client.getApp(appId, function (error, app) {
if (error) return Client.error(error);
$scope.app = app;
asyncSeries([
fetchUsers,
fetchGroups,
getDomains,
getBackupConfig
], function (error) {
if (error) return Client.error(error);
$scope.display.show();
$scope.location.show();
$scope.resources.show();
$scope.access.show();
$scope.email.show();
$scope.security.show();
$scope.backups.show();
$scope.updates.show();
$scope.ready = true;
});
});
});
// setup all the dialog focus handling
['appConfigureModal', 'appUninstallModal', 'appUpdateModal', 'appRestoreModal', 'appInfoModal', 'appErrorModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find("[autofocus]:first").focus();
});
});
$('.modal-backdrop').remove();
}]);

View File

@@ -608,7 +608,7 @@
<a href="" ng-show="app | activeTask" ng-click="appCancel.show(app)" uib-tooltip="Cancel {{app | installationStateCancelTooltip}}" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-ban scale"></i></a>
<a href="" ng-hide="app | activeTask" ng-click="appUninstall.show(app)" uib-tooltip="Uninstall" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-times scale"></i></a>
<a href="" ng-hide="!backupsEnabled || (app | activeTask)" ng-click="appRestore.show(app)" uib-tooltip="Backups" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-archive scale"></i></a>
<a href="" ng-hide="(app | activeTask) || (app | installError)" ng-click="appConfigure.show(app)" uib-tooltip="Configure" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-pencil-alt scale"></i></a>
<a ng-href="#/app/{{ app.id}}" ng-hide="(app | activeTask) || (app | installError)" uib-tooltip="Configure" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-pencil-alt scale"></i></a>
<a href="" ng-hide="(app | activeTask) || !(app | installError)" ng-click="appConfigure.show(app)" uib-tooltip="Repair" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-wrench scale"></i></a>
<a ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank" uib-tooltip="Terminal" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-terminal scale"></i></a>
<a ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank" uib-tooltip="Logs" tooltip-placement="right" tooltip-class="app-tooltip"><i class="fa fa-file-alt scale"></i></a>

View File

@@ -36,14 +36,14 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
certificateFileName: '',
keyFile: null,
keyFileName: '',
memoryLimit: 0,
memoryTicks: [],
mailboxName: '',
accessRestrictionOption: 'any',
accessRestriction: { users: [], groups: [] },
dataDir: null,
alternateDomains: [],
mailboxNameEnabled: false,
memoryLimit: 0,
memoryTicks: [],
dataDir: null,
dataDirEnabled: false,
ssoAuth: false,
ftp: false,
@@ -100,7 +100,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.mailboxName = app.mailboxName || '';
$scope.appConfigure.label = app.label || '';
$scope.appConfigure.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oauth']) && app.sso;
$scope.appConfigure.ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
@@ -114,6 +113,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.memoryTicks.unshift(app.manifest.memoryLimit);
}
$scope.appConfigure.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oauth']) && app.sso;
$scope.appConfigure.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
$scope.appConfigure.accessRestriction = { users: [], groups: [] };