Files
cloudron-box/src/views/app.html
2020-05-14 16:09:59 -07:00

1066 lines
67 KiB
HTML

<script>
function imageErrorHandler(elem) {
'use strict';
elem.src = elem.getAttribute('fallback-icon');
elem.onerror = null; // avoid retry after default icon cannot be loaded
}
</script>
<!-- Modal postinstall confirm -->
<div class="modal fade" id="postInstallConfirmModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-info-icon"/>
<h5 class="app-info-title">
{{ app.manifest.title }}
<span class="app-info-meta text-small">Package <a ng-href="/#/appstore/{{ app.manifest.id }}?version={{ app.manifest.version }}">v{{ app.manifest.version }}</a> </span>
<br/>
<span ng-show="app.manifest.documentationUrl"><a target="_blank" ng-href="{{ app.manifest.documentationUrl }}">Documentation</a> </span>
<br/>
</h5>
</div>
<div class="modal-body">
<div ng-bind-html="app.manifest.postInstallMessage | postInstallMessage:app | markdown2html"></div>
<div ng-show="app.manifest.documentationUrl">
Please see the <a target="_blank" ng-href="{{ app.manifest.documentationUrl }}">documentation</a> for more information.
</div>
</div>
<div class="modal-footer">
<div class="form-group pull-left">
<input type="checkbox" id="postInstallConfirmCheckbox" ng-model="postInstallConfirm.confirmed">
<label class="control-label" for="postInstallConfirmCheckbox">Acknowledge instructions</label>
</div>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<a class="btn btn-success" ng-href="{{ postInstallConfirm.confirmed ? ('https://' + app.fqdn) : '' }}" target="_blank" ng-disabled="!postInstallConfirm.confirmed" ng-click="postInstallConfirm.submit()"><i class="fas fa-external-link-alt"></i> Open App</a>
</div>
</div>
</div>
</div>
<!-- Modal uninstall app -->
<div class="modal fade" id="uninstallModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Uninstall {{ app.label || app.fqdn }}</h4>
</div>
<div class="modal-body">
<p>This will immediately uninstall <b>{{ app.label || app.fqdn }}</b> and remove all it's data.</p>
</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="uninstall.submit()" ng-disabled="uninstall.busy"><i class="fa fa-circle-notch fa-spin" ng-show="uninstall.busy"></i> Uninstall</button>
</div>
</div>
</div>
</div>
<!-- Modal domain collision -->
<div class="modal fade" id="domainCollisionsModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Domain Collision</h4>
</div>
<div class="modal-body">
<p>The following domains already exist in your DNS:</p>
<ul>
<li ng-repeat="domain in location.domainCollisions">{{ domain.subdomain + '.' + domain.domain }}</li>
</ul>
<p>As a precautionary measure, Cloudron does not overwrite existing DNS records. Please confirm that the above domains are not in use for services external to Cloudron.</p>
</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="location.submit(true)">Overwrite existing DNS Records</button>
</div>
</div>
</div>
</div>
<!-- Modal repair -->
<div class="modal fade" id="repairModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Repair {{ app.fqdn }}</h4>
</div>
<div class="modal-body">
<div ng-if="!app.error">
<p>Cloudron will re-install the app in-place with existing configuration. Existing data will be retained.</p>
</div>
<div ng-if="app.error">
<p>The <b>{{ app.error.installationState | taskName }}</b> operation failed with the following error:</p>
<p class="text-danger">{{ app.error.reason + ': ' + app.error.message }}</p>
</div>
<div class="form-group" ng-show="repair.location && repair.domain">
<p>Cloudron will repair the app to use the following domains:</p>
<label class="control-label">Location</label>
<div class="input-group form-inline">
<input type="text" class="form-control" ng-model="repair.location" 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>{{ (!repair.location ? '' : (repair.domain.config.hyphenatedSubdomains ? '-' : '.')) + repair.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="repair.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</div>
<div ng-show="repair.alternateDomains.length">
<p ng-repeat="alternateDomain in repair.alternateDomains">
<label class="control-label"><input type="checkbox" ng-model="alternateDomain.enabled">
{{ alternateDomain.subdomain + (!alternateDomain.subdomain ? '' : (alternateDomain.domain.config.hyphenatedSubdomains ? '-' : '.')) + alternateDomain.domain.domain }}
</label>
</p>
</div>
<div ng-show="repair.backups.length">
<label class="control-label">Restore from Backup:</label>
<select class="form-control" ng-model="repair.backupId">
<option ng-repeat="backup in repair.backups" value="{{ backup.id }}">{{ backup.creationTime | prettyDate }} - v{{ backup.packageVersion }}</option>
</select>
</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="repair.submit()" ng-disabled="repair.retryBusy">
<i class="fa fa-circle-notch fa-spin" ng-show="repair.retryBusy"></i> Retry {{ app.error.installationState | taskName }}
</button>
</div>
</div>
</div>
</div>
<!-- modal import backup -->
<div class="modal fade" id="importBackupModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Import Backup</h4>
</div>
<div class="modal-body">
<p class="text-info">Any data generated between now and the last known backup will be irrevocably lost.
It is recommended to create a backup of the current data before attempting an import.
</p>
<form name="importBackupForm" role="form" novalidate ng-submit="importBackup.submit()" autocomplete="off">
<fieldset>
<p class="has-error text-center" ng-show="backups.error">{{ importBackup.error.generic }}</p>
<div class="form-group">
<label class="control-label" for="storageProvider">Storage provider <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#storage-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<select class="form-control" id="storageProvider" ng-model="importBackup.provider" ng-options="a.value as a.name for a in storageProvider" ng-change="importBackup.clearForm()" ng-disabled="importBackup.busy"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.key }">
<label ng-show="importBackup.provider !== 'filesystem'" class="control-label" for="inputImportBackupId">Backup ID</label>
<label ng-show="importBackup.provider === 'filesystem'" class="control-label" for="inputImportBackupId">Backup Path</label>
<input type="text" class="form-control" ng-model="importBackup.backupId" id="inputImportBackupId" ng-disabled="importBackup.busy" required>
</div>
<!-- S3/Minio/SOS/GCS -->
<div class="form-group" ng-class="{ 'has-error': importBackup.error.endpoint }" ng-show="importBackup.provider === 'minio' || importBackup.provider === 's3-v4-compat'">
<label class="control-label" for="inputimportBackupEndpoint">Endpoint</label>
<input type="text" class="form-control" ng-model="importBackup.endpoint" id="inputimportBackupEndpoint" name="endpoint" ng-disabled="importBackup.busy" placeholder="URL of Minio/S3 Compatible" ng-required="importBackup.provider === 'minio' || importBackup.provider === 's3-v4-compat'">
</div>
<div class="checkbox" ng-show="importBackup.provider === 'minio' || importBackup.provider === 's3-v4-compat'" >
<label>
<input type="checkbox" ng-model="importBackup.acceptSelfSignedCerts" id="inputimportBackupSelfSigned">Accept Self-signed certificate</input>
</label>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.bucket }" ng-show="s3like(importBackup.provider) || importBackup.provider === 'gcs'">
<label class="control-label" for="inputImportBackupBucket">Bucket name</label>
<input type="text" class="form-control" ng-model="importBackup.bucket" id="inputImportBackupBucket" name="bucket" ng-disabled="importBackup.busy" ng-required="s3like(importBackup.provider)">
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.prefix }" ng-show="importBackup.provider !== 'filesystem' && importBackup.provider !== ''">
<label class="control-label" for="inputimportBackupPrefix">Prefix</label>
<input type="text" class="form-control" ng-model="importBackup.prefix" id="inputimportBackupPrefix" name="prefix" ng-disabled="importBackup.busy" placeholder="Prefix for backup file names">
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 's3'">
<label class="control-label" for="inputImportBackupS3Region">Region</label>
<select class="form-control" name="region" id="inputImportBackupS3Region" ng-model="importBackup.region" ng-options="a.value as a.name for a in s3Regions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 's3'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'digitalocean-spaces'">
<label class="control-label" for="inputImportBackupDORegion">Region</label>
<select class="form-control" name="region" id="inputImportBackupDORegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in doSpacesRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'digitalocean-spaces'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'exoscale-sos'">
<label class="control-label" for="inputimportBackupExoscaleRegion">Region</label>
<select class="form-control" name="region" id="inputimportBackupExoscaleRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in exoscaleSosRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'exoscale-sos'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'wasabi'">
<label class="control-label" for="inputimportBackupWasabiRegion">Region</label>
<select class="form-control" name="region" id="inputimportBackupWasabiRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in wasabiRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'wasabi'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'scaleway-objectstorage'">
<label class="control-label" for="inputimportBackupScalewayRegion">Region</label>
<select class="form-control" name="region" id="inputimportBackupScalewayRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in scalewayRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'scaleway-objectstorage'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'linode-objectstorage'">
<label class="control-label" for="inputimportBackupLinodeRegion">Region</label>
<select class="form-control" name="region" id="inputimportBackupLinodeRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in linodeRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'linode-objectstorage'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.region }" ng-show="importBackup.provider === 'ovh-objectstorage'">
<label class="control-label" for="inputimportBackupOvhRegion">Region</label>
<select class="form-control" name="region" id="inputimportBackupOvhRegion" ng-model="importBackup.endpoint" ng-options="a.value as a.name for a in ovhRegions" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'ovh-objectstorage'"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.accessKeyId }" ng-show="s3like(importBackup.provider)">
<label class="control-label" for="inputImportBackupAccessKeyId">Access key id</label>
<input type="text" class="form-control" ng-model="importBackup.accessKeyId" id="inputImportBackupAccessKeyId" name="accessKeyId" ng-disabled="importBackup.busy" ng-required="s3like(importBackup.provider)">
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.secretAccessKey }" ng-show="s3like(importBackup.provider)">
<label class="control-label" for="inputImportBackupSecretAccessKey">Secret access key</label>
<input type="text" class="form-control" ng-model="importBackup.secretAccessKey" id="inputImportBackupSecretAccessKey" name="secretAccessKey" ng-disabled="importBackup.busy" ng-required="s3like(importBackup.provider)">
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.gcsKeyInput }" ng-show="importBackup.provider === 'gcs'">
<label class="control-label" for="gcsKeyInput">Service Account Key</label>
<div class="input-group">
<input type="file" id="gcsKeyFileInput" style="display:none"/>
<input type="text" class="form-control" placeholder="Service Account Key" ng-model="importBackup.gcsKey.keyFileName" id="gcsKeyInput" name="cert" onclick="getElementById('gcsKeyFileInput').click();" style="cursor: pointer;" ng-disabled="importBackup.busy" ng-required="importBackup.provider === 'gcs'">
<span class="input-group-addon">
<i class="fa fa-upload" onclick="getElementById('gcsKeyFileInput').click();"></i>
</span>
</div>
</div>
<div class="form-group">
<label class="control-label" for="storageFormat">Storage Format <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#backup-formats" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<select class="form-control" id="storageFormat" ng-change="importBackup.password = ''" ng-model="importBackup.format" ng-options="a.value as a.name for a in formats" ng-disabled="importBackup.busy"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': importBackup.error.password }">
<label class="control-label" for="inputImportBackupPassword">Encryption password (optional) <sup><a ng-href="{{ config.webServerOrigin }}/documentation/backups/#encryption" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<input type="text" class="form-control" ng-model="importBackup.password" id="inputImportBackupPassword" ng-disabled="importBackup.busy" placeholder="Passphrase used to encrypt the backups">
</div>
<input class="ng-hide" type="submit" ng-disabled="importBackupForm.$invalid"/>
</fieldset>
</form>
</div>
<div class="modal-footer ">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-outline btn-success pull-right" ng-click="importBackup.submit()" ng-disabled="importBackupForm.$invalid || importBackup.busy"><i class="fa fa-circle-notch fa-spin" ng-show="importBackup.busy"></i> Import</button>
</div>
</div>
</div>
</div>
<!-- Modal postinstall -->
<div class="modal fade" id="postInstallModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4>First Time Setup</h4>
</div>
<div class="modal-body">
<div ng-bind-html="app.manifest.postInstallMessage | postInstallMessage:app | markdown2html"></div>
</div>
<div class="modal-footer">
<div class="form-group pull-left" ng-show="postInstallMessage.openApp">
<input type="checkbox" id="appPostInstallConfirmCheckbox" ng-model="postInstallMessage.confirmed">
<label class="control-label" for="appPostInstallConfirmCheckbox">Acknowledge instructions</label>
</div>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<a class="btn btn-success" ng-href="{{ postInstallMessage.confirmed ? ('https://' + app.fqdn) : '' }}" target="_blank" ng-disabled="!postInstallMessage.confirmed" ng-click="postInstallMessage.submit()" ng-show="postInstallMessage.openApp">Open {{ app.manifest.title }}</a>
</div>
</div>
</div>
</div>
<!-- Modal update app -->
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Update {{ app.fqdn }}</h4>
</div>
<div class="modal-body">
<p>Changes for new version <b>{{ config.update.apps[app.id].manifest.version}}</b>:</p>
<div ng-bind-html="config.update.apps[app.id].manifest.changelog | markdown2html"></div>
<p class="text-danger text-bold" ng-show="!config.update.apps[app.id].manifest.dockerImage">
<br/>
Your Cloudron subscription has expired. Please setup a subscription to update the app.
</p>
</div>
<div class="modal-footer">
<label class="checkbox-inline pull-left" ng-show="config.update.apps[app.id].manifest.dockerImage">
<input type="checkbox" ng-model="updates.skipBackup"><b>Skip backup</b>
</label>
<button type="button" class="btn btn-primary pull-left" ng-show="!config.update.apps[app.id].manifest.dockerImage && user.isAtLeastOwner" ng-click="openSubscriptionSetup()">Setup Subscription</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="updates.confirmUpdate()" ng-disabled="!config.update.apps[app.id].manifest.dockerImage || updates.busyUpdate"><i class="fa fa-circle-notch fa-spin" ng-show="updates.busyUpdate"></i> Update</button>
</div>
</div>
</div>
</div>
<!-- Modal restore app -->
<div class="modal fade" id="restoreModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
Restore - {{ app.fqdn }}
</h4>
</div>
<div class="modal-body" style="padding: 0 15px">
<p>This will restore this app to the data from <b>{{ restore.backup.creationTime | prettyDate }}</b>.</p>
<p class="text-danger">Any data generated between now and the last known backup will be irrevocably lost.
It is recommended to create a backup of the current data before attempting a restore.
</p>
<br/>
</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="restore.submit()"><i class="fas fa-history" ng-hide="restore.busy"></i><i class="fa fa-circle-notch fa-spin" ng-show="clone.busy"></i> Restore</button>
</div>
</div>
</div>
</div>
<!-- Modal clone app -->
<div class="modal fade" id="cloneModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
Clone - {{ app.fqdn }}
</h4>
</div>
<div class="modal-body" style="padding: 0 15px">
<p>Using backup from <b>{{ clone.backup.creationTime | prettyDate }}</b> and version <b>v{{ clone.backup.packageVersion }}</b></p>
<form role="form" ng-submit="clone.submit()" autocomplete="off">
<fieldset>
<div class="form-group" ng-class="{ 'has-error': clone.error.location }">
<label class="control-label" for="cloneLocationInput">Location</label>
<div ng-show="clone.error.location"><small>{{ clone.error.location }}</small></div>
<div class="input-group form-inline">
<input type="text" class="form-control" ng-model="clone.location" id="cloneLocationInput" 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>{{ (!clone.location ? '' : (clone.domain.config.hyphenatedSubdomains ? '-' : '.')) + clone.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="clone.domain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
</div>
<p class="small text-center text-warning" ng-show="clone.domain.provider === 'linode'">
<b>Linode DNS average <a target="_blank" ng-href="{{ config.webServerOrigin }}/documentation/domains/#linode-dns">propagation time</a> is 30 minutes.
Cloning the app will take a while.</b>
<br>
</p>
<p class="text-center" ng-show="clone.location && clone.domain.provider === 'manual'">
<b>Add an A record manually for {{ clone.location }} to this Cloudron's public IP</b>
<br>
</p>
<div class="has-error text-center" ng-show="clone.error.port">{{ clone.error.port }}</div>
<div ng-repeat="(env, info) in clone.portBindingsInfo">
<ng-form name="portInfo_form">
<div class="form-group" ng-class="{ 'has-error': (!clone.itemName{{$index}}.$dirty && clone.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="clone.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="clone.portBindings[env]" ng-disabled="!clone.portBindingsEnabled[env]" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{hostPortMin}}" max="{{hostPortMax}}" required>
</div>
</ng-form>
</div>
</fieldset>
</form>
</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-click="clone.submit()"><i class="far fa-clone" ng-hide="clone.busy"></i><i class="fa fa-circle-notch fa-spin" ng-show="clone.busy"></i> Clone</button>
</div>
</div>
</div>
</div>
<div class="content content-large app-configure">
<a href="/#/apps" class="back-to-view-link"><i class="fas fa-arrow-left"></i> Back to My Apps</a>
<br/>
<div class="row" ng-show="view">
<div class="col-sm-2 text-center">
<img ng-src="{{ app.iconUrl || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)" class="app-icon"/>
</div>
<div class="col-sm-8">
<div class="app-header-container">
<h1>
<a ng-href="{{ app | applicationLink }}" target="_blank" ng-class="{ 'hand': (app | appIsInstalledAndHealthy) }" ng-click="(app | appIsInstalledAndHealthy) && app.pendingPostInstallConfirmation && postInstallMessage.show(true)">{{ app.label || app.fqdn }} <sup ng-show="app | appIsInstalledAndHealthy"><i class="fas fa-external-link-alt" style="font-size: 12px;"></i></sup></a>
<br/>
<span class="text-small">{{ app | installationStateLabel }} {{ app.taskProgressMessage ? ' - ' + app.taskProgressMessage : '' }}</span>
<span class="text-small" ng-show="app.error"> : {{ app.error.reason + ' - ' + app.error.message }}</span>
</h1>
<div>
<div class="dropdown">
<button class="btn btn-sm btn-info dropdown-toggle" type="button" data-toggle="dropdown">
Documentation
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li ng-class="{ 'disabled': !app.manifest.postInstallMessage }"><a href="" ng-click="postInstallMessage.show(false)">First Time Setup</a></li>
<li ng-class="{ 'disabled': !app.manifest.documentationUrl }"><a ng-href="{{ app.manifest.documentationUrl }}" target="_blank">Documentation</a></li>
<li role="separator" class="divider"></li>
<li ng-class="{ 'disabled': !app.manifest.website }"><a ng-href="{{ app.manifest.website }}" target="_blank">Project Website</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="row" ng-show="app.taskId">
<div class="col-sm-8 col-sm-offset-2" style="height: 10px; display: flex; align-items: center;">
<div class="progress progress-striped active animateMeOpacity" style="height: 10px; flex-grow: 1;">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.taskProgress }}%"></div>
</div>
<div ng-show="app.taskMinutesActive >= 5" class="text-danger hand" style="margin: 0 4px;" ng-click="stopAppTask(app.taskId)" uib-tooltip="Cancel Task"><i class="fas fa-times"></i></div>
</div>
</div>
<div class="row" ng-hide="view">
<div class="col-md-12 text-center">
<br/><br/><h2><i class="fa fa-circle-notch fa-spin"></i></h2>
</div>
</div>
<div class="row app-configure-links-container" ng-show="view">
<div class="col-sm-2">
<div class="app-configure-links">
<div ng-click="setView('display')" ng-class="{ 'active': view === 'display' }">Display</div>
<div ng-click="setView('location')" ng-class="{ 'active': view === 'location' }">Location</div>
<div ng-click="setView('access')" ng-class="{ 'active': view === 'access' }">Access Control</div>
<div ng-click="setView('resources')" ng-class="{ 'active': view === 'resources' }">Resources</div>
<div ng-click="setView('graphs')" ng-class="{ 'active': view === 'graphs' }">Graphs</div>
<div ng-click="setView('security')" ng-class="{ 'active': view === 'security' }">Security</div>
<div ng-click="setView('email')" ng-class="{ 'active': view === 'email' }" ng-show="app.manifest.addons.sendmail || app.manifest.addons.recvmail">Email</div>
<div ng-click="setView('updates')" ng-class="{ 'active': view === 'updates' }">Updates</div>
<div ng-click="setView('backups')" ng-class="{ 'active': view === 'backups' }">Backups</div>
<div ng-click="setView('console')" ng-class="{ 'active': view === 'console' }">Console</div>
<div ng-click="setView('repair')" ng-class="{ 'active': view === 'repair' }">Repair</div>
<div ng-click="setView('uninstall')" ng-class="{ 'active': view === 'uninstall' }">Uninstall</div>
</div>
</div>
<div class="col-sm-8 card-container">
<div class="card" ng-show="view === 'display'">
<div class="row">
<div class="col-md-12">
<form role="form" name="displayForm" ng-submit="display.submit()" autocomplete="off">
<fieldset>
<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 space 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()">
<img ng-src="{{ display.iconUrl() || 'img/appicon_fallback.png' }}" fallback-icon="img/appicon_fallback.png" onerror="imageErrorHandler(this)"/>
<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="(!display.icon.data && !displayForm.$dirty) || displayForm.$invalid || display.busy"/>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="display.submit()" ng-disabled="(!display.icon.data && !displayForm.$dirty) || display.$invalid || display.busy"><i class="fa fa-circle-notch fa-spin" ng-show="display.busy"></i> Save</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'location'">
<div class="row">
<div class="col-md-12">
<form role="form" name="locationForm" ng-submit="location.submit()" autocomplete="off">
<fieldset>
<div class="form-group" ng-class="{ 'has-error': (locationForm.location.$dirty && locationForm.location.$invalid) || (!locationForm.location.$dirty && location.error.location) }">
<label class="control-label">Location</label>
<div class="has-error" ng-show="location.error.location">{{ location.error.location }}</div>
<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="small text-center text-warning" ng-show="location.domain.provider === 'linode'">
<b>Linode DNS average <a target="_blank" ng-href="{{ config.webServerOrigin }}/documentation/domains/#linode-dns">propagation time</a> is 30 minutes.
Changing the location will take a while.</b>
<br>
</p>
<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>
<!-- hidden submit has to be prior to other button elements, otherwise firefox will treat them as the "enter" key action, in this case the alternate domain delete button! -->
<input class="ng-hide" type="submit" ng-disabled="locationForm.$invalid || location.busy"/>
<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>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="location.submit()" ng-disabled="location.$invalid || location.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i class="fa fa-circle-notch fa-spin" ng-show="location.busy"></i> Save
</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'access'">
<div class="row">
<div class="col-md-12">
<form role="form" name="accessForm" ng-submit="access.submit()" autocomplete="off">
<fieldset>
<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 ng-href="{{ config.webServerOrigin }}/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" filter-after-rows="5" scroll-after-rows="10"></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" filter-after-rows="5" scroll-after-rows="10"></multiselect>
</div>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="(access.accessRestrictionOption === 'groups' && !access.isAccessRestrictionValid()) || accessForm.$invalid || access.busy"/>
</div>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="access.submit()" ng-disabled="(access.accessRestrictionOption === 'groups' && !access.isAccessRestrictionValid()) || access.$invalid || access.busy"><i class="fa fa-circle-notch fa-spin" ng-show="access.busy"></i> Save</button>
</div>
</div>
<div class="row" ng-show="app.manifest.addons.localstorage.ftp">
<hr/>
<div class="col-md-12">
<label>SFTP</label> <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#ftp-access" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup><br/>
Server: {{ config.adminFqdn }}<br/>
Port: 222<br/>
Username: {{ user.username }}@{{ app.fqdn }}<br/>
</div>
</div>
</div>
<div class="card" ng-show="view === 'resources'">
<div class="row">
<div class="col-md-12">
<form role="form" name="resourcesForm" ng-submit="resources.submitMemoryLimit()" autocomplete="off">
<fieldset>
<div class="form-group">
<label class="control-label" for="memoryLimit">Memory Limit <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#memory-limit" 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>
<p>Cloudron allocates 50% of this value as RAM and 50% as swap.</p>
<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>
<input class="ng-hide" type="submit" ng-disabled="resources.memoryLimit === resources.currentMemoryLimit || resourcesForm.$invalid || resources.busy"/>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="resources.submitMemoryLimit()" ng-disabled="resources.memoryLimit === resources.currentMemoryLimit || resourcesForm.$invalid || resources.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i class="fa fa-circle-notch fa-spin" ng-show="resources.busy"></i> Resize
</button>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
<form role="form" name="resourcesForm" ng-submit="resources.submitCpuShares()" autocomplete="off">
<fieldset>
<div class="form-group">
<label class="control-label" for="cpuShares">CPU Shares <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#cpu-shares" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> : <b>{{ (resources.cpuShares * 100 / 1024 | number:0) + ' %' }}</b></label>
<p>Percent of CPU time when system is under heavy load.</p>
<div style="padding: 0 10px;">
<slider id="cpuShares" ng-model="resources.cpuShares" ticks="[32, 256, 512, 768, 1024]" step="32" ticks-snap-bounds="32" min="32" max="1024" tooltip="hide"></slider>
</div>
</div>
<input class="ng-hide" type="submit" ng-disabled="resources.cpuShares === resources.currentCpuShares || resourcesForm.$invalid || resources.busyCpuShares"/>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="resources.submitCpuShares()" ng-disabled="resources.cpuShares === resources.currentCpuShares || resourcesForm.$invalid || resources.busyCpuShares || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i class="fa fa-circle-notch fa-spin" ng-show="resources.busyCpuShares"></i> Set
</button>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
<label class="control-label" for="resourcesEnableDataDir">Storage <sup><a ng-href="{{ config.webServerOrigin }}/documentation/storage/#app-data-directory" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<p>
By default, this app's data is located at <code>/home/yellowtent/appsdata/{{ app.id }}</code>. If the server is running out of disk space,
you can bind an external disk and move this app's data there. Only Ext4 and NFS binds are supported.
</p>
<form role="form" name="resourcesDataDirForm" ng-submit="resources.submitDataDir()" autocomplete="off">
<fieldset>
<div class="form-group" ng-class="{ 'has-error': resourcesDataDirForm.$dirty && resources.error.dataDir }">
<div ng-show="resources.error.dataDir">{{ resources.error.dataDir }}</div>
<input type="text" class="form-control" name="dataDir" placeholder="Leave empty to use platform default" ng-model="resources.dataDir">
</div>
<input class="ng-hide" type="submit" ng-disabled="!resourcesDataDirForm.$dirty || resourcesDataDirForm.$invalid || resources.busyDataDir"/>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="resources.submitDataDir()" ng-disabled="!resourcesDataDirForm.$dirty || resourcesDataDirForm.$invalid || resources.busyDataDir || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i class="fa fa-circle-notch fa-spin" ng-show="resources.busyDataDir"></i> Move Data
</button>
</div>
</div>
<hr/>
<!-- hidden for now until we find good use cases -->
<div class="form-group binds" ng-hide="true">
<label class="control-label">Binds <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#volumes" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
<div class="has-error" ng-show="resources.error.binds">{{ resources.error.binds }}</div>
<div ng-repeat="bind in resources.binds">
<form class="form-inline">
<div class="form-group bind-name">
<div class="input-group">
<span class="input-group-addon">/media/</span>
<input type="text" class="form-control" ng-model="bind.name" placeholder="Container path">
</div>
</div>
<div class="form-group bind-host-path">
<input type="text" class="form-control" ng-model="bind.hostPath" placeholder="Host path">
</div>
<div class="checkbox">
<label>
<input type="checkbox" ng-model="bind.readOnly"> Read Only
</label>
</div>
<button class="btn btn-danger btn-xs" ng-click="resources.delBind($event, $index)"><i class="far fa-trash-alt"></i></button>
</form>
</div>
<div ng-show="resources.binds.length === 0">
No binds are configured. <a href="" ng-click="resources.addBind($event)">Add an external bind</a>
</div>
<div ng-show="resources.binds.length > 0" style="margin-top: 5px;">
<a href="" ng-click="resources.addBind($event)">Add another bind</a>
</div>
</div>
<!-- hidden for now until we find good use cases -->
<div class="row" ng-hide="true">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="resources.submitBinds()" ng-disabled="resources.busyBinds || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i class="fa fa-circle-notch fa-spin" ng-show="resources.busyBinds"></i> Save
</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'graphs'">
<div class="row">
<div class="col-md-12">
<div class="dropdown pull-right">
<button class="btn btn-sm btn-primary dropdown-toggle" type="button" data-toggle="dropdown">
Select Period {{ graphs.periodLabel }}
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a href="" ng-click="graphs.setPeriod(6, '6 hours')">6 hours</a></li>
<li><a href="" ng-click="graphs.setPeriod(24, '24 hours')">24 hours</a></li>
<li><a href="" ng-click="graphs.setPeriod(24*7, '7 days')">7 days</a></li>
<li><a href="" ng-click="graphs.setPeriod(24*30, '30 days')">30 days</a></li>
</ul>
</div>
<label style="margin-top: 10px;">Main Memory + Swap Usage in Megabytes</label>
<canvas id="graphsMemoryChart" style="width: 100%;"></canvas>
<label>Disk Usage in Megabytes</label>
<canvas id="graphsDiskChart" style="width: 100%;"></canvas>
</div>
</div>
</div>
<div class="card" ng-show="view === 'email'">
<div class="row">
<div class="col-md-12">
<label class="control-label" for="emailMailboxNameEnabled">Mail FROM Address</label>
<p>This sets the address from which this app sends email. This app is already configured to send mail using {{app.domain}}'s <a ng-href="/#/email/{{ app.domain }}">Outbound Email</a> settings.</p>
<form role="form" name="emailForm" ng-submit="email.submit()" autocomplete="off">
<fieldset>
<!-- recvmail currently only works with cloudron email -->
<div class="form-group" ng-class="{ 'has-error': emailForm.$dirty && email.error.mailboxName }">
<div ng-show="email.error.mailboxName">{{ email.error.mailboxName }}</div>
<div class="input-group form-inline" ng-class="{ 'has-error': !emailForm.mailboxName.$dirty && email.error.mailboxName }">
<input type="text" class="form-control" name="mailboxName" placeholder="Leave empty to use platform default" ng-model="email.mailboxName">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span>{{ '@' + email.mailboxDomain.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="email.mailboxDomain = domain">{{ domain.domain }}</a>
</li>
</ul>
</div>
</div>
<br/>
</div>
<input class="ng-hide" type="submit" ng-disabled="(email.currentMailboxDomainName === email.mailboxDomain.domain && email.currentMailboxName === email.mailboxName) || email.busy || app.error || app.taskId"/>
</fieldset>
</form>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="email.submit()" ng-disabled="(email.currentMailboxDomainName === email.mailboxDomain.domain && email.currentMailboxName === email.mailboxName) || email.busy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i class="fa fa-circle-notch fa-spin" ng-show="email.busy"></i> Save
</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'security'">
<div class="row">
<div class="col-md-12">
<form role="form" name="securityForm" ng-submit="security.submit()" autocomplete="off">
<fieldset>
<div class="form-group">
<label class="control-label" style="width: 100%">Robots.txt <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#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 = ROBOTS_DISABLE_INDEXING_TEMPLATE">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>
<div class="form-group">
<label class="control-label" style="width: 100%">Content Security Policy <sup><a ng-href="{{ config.webServerOrigin }}/documentation/apps/#custom-csp" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup> </label>
<p>Setting this option will override any CSP headers sent by the app itself</p>
<textarea ng-model="security.csp" placeholder="default-src 'self'; frame-ancestors 'none';" class="form-control" rows="2"></textarea>
</div>
<input class="ng-hide" type="submit" ng-disabled="securityForm.$invalid || security.busy"/>
</fieldset>
</form>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-outline btn-primary pull-right" ng-click="security.submit()" ng-disabled="security.$invalid || security.busy || app.error" tooltip-enable="app.error" uib-tooltip="App is in error state">
<i class="fa fa-circle-notch fa-spin" ng-show="security.busy"></i> Save
</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'updates'">
<div class="row">
<div class="col-md-12">
<label class="control-label">Check For Updates</label>
<p ng-show="app.appStoreId">This app is running {{ app.manifest.title }} {{ app.upstreamVersion }} (Package <a ng-href="/#/appstore/{{app.manifest.id}}?version={{app.manifest.version}}">v{{ app.manifest.version }}</a>) and was last updated <code>{{ app.updateTime | prettyDate }}</code>.</p>
<p ng-show="!app.appStoreId">This app is running <code>{{ app.manifest.dockerImage }}</code> (Package v{{ app.manifest.version }}) and was last updated <code>{{ app.updateTime | prettyDate }}</code>.</p>
<br/>
<button class="btn btn-primary pull-right" uib-tooltip="{{ app.appStoreId ? '' : 'Not available for custom apps' }}" ng-disabled="!app.appStoreId" ng-click="updates.check()" ng-hide="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version" ng-disabled="updates.busyCheck"><i class="fa fa-circle-notch fa-spin" ng-show="updates.busyCheck"></i> Check for Updates</button>
<button class="btn btn-success pull-right" ng-click="updates.askUpdate()" ng-show="app.installationState !== 'pending_update' && config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version" ng-disabled="app.taskId || app.error" tooltip-enable="app.error" uib-tooltip="App is in error state">
Update Available
</button>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
<label class="control-label">Automatic Updates</label>
<p>Cloudron periodically polls the App Store to check for updates. If you disable automatic updates, be sure to manually check for updates.</p>
<p>Automatic Updates is currently <b>{{ updates.enableAutomaticUpdate ? 'enabled' : 'disabled' }}</b>.</p>
<button class="btn btn-primary pull-right" uib-tooltip="{{ app.appStoreId ? '' : 'Not available for custom apps' }}" ng-class="{ 'btn-danger': updates.enableAutomaticUpdate }" ng-click="updates.toggleAutomaticUpdates()" ng-disabled="updates.busy || !app.appStoreId"><i class="fa fa-circle-notch fa-spin" ng-show="updates.busy"></i> {{ updates.enableAutomaticUpdate ? 'Disable' : 'Enable' }} Automatic Updates</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'backups'">
<div class="row">
<div class="col-md-12">
<label class="control-label">Backups</label>
<div>
<span>Backups are complete snapshots of the app. You can use app backups to restore or clone this app.</span>
</div>
<br/>
<!-- backup id copy helper -->
<input type="text" class="offscreen" aria-hidden="true" id="backupIdHelper" value="">
<table ng-hide="!backups.backups.length" class="table table-hover" style="margin: 0;">
<thead>
<tr>
<th width="25px">&nbsp;</th>
<th>Backup</th>
<th class="text-right" width="180px">Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="backup in backups.backups">
<td><div ng-click="backups.copyBackupId(backup)" class="hand" uib-tooltip="{{ backups.copyBackupIdDone ? 'Copied to clipboard' : 'Click to copy backup id' }}" tooltip-placement="right"><i class="fa fa-copy"></i></div></td>
<td><div uib-tooltip="{{ backup.creationTime | prettyLongDate }}">v{{ backup.packageVersion }} - {{ backup.creationTime | prettyDate }}</div></td>
<td class="text-right no-wrap" style="vertical-align: bottom">
<button class="btn btn-xs btn-default" ng-click="clone.show(backup)" uib-tooltip="Clone from this Backup"><i class="far fa-clone"></i></button>
<button class="btn btn-xs btn-danger" ng-click="restore.show(backup)" ng-disabled="app.taskId" uib-tooltip="Restore to this Backup"><i class="fas fa-history"></i></button>
</td>
</tr>
</tbody>
</table>
<br/>
<button type="button" class="btn btn-primary pull-right" ng-click="backups.createBackup()" ng-disabled="app.taskId || backups.busyCreate || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i class="fa fa-circle-notch fa-spin" ng-show="app.installationState === 'pending_backup' || backups.busyCreate"></i> Create Backup
</button>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
<label class="control-label">Import From External Backup</label>
<p>Use this to migrate an app from another Cloudron. The other app must have the same package version and access
control setting as this one.
</p>
<button class="btn btn-primary pull-right" class="btn-primary" ng-click="importBackup.show()" ng-disabled="importBackup.busy || app.taskId" tooltip-enable="app.taskId" uib-tooltip="App is busy">
<i class="fa fa-circle-notch fa-spin" ng-show="backups.busy"></i> Import Backup
</button>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
<label class="control-label">Automatic Backups</label>
<p>Cloudron periodically creates a backup based on the <a href="/#/backups">backup</a> settings. Automatic Backups is currently <b>{{ backups.enableBackup ? 'enabled' : 'disabled' }}</b>.</p>
<button class="btn btn-primary pull-right" ng-class="{ 'btn-danger': backups.enableBackup }" ng-click="backups.toggleAutomaticBackups()" ng-disabled="backups.busy"><i class="fa fa-circle-notch fa-spin" ng-show="backups.busy"></i> {{ backups.enableBackup ? 'Disable' : 'Enable' }} Automatic Backups</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'console'">
<div class="row">
<div class="col-md-12">
<label class="control-label">Console Access</label>
<p>This will open a console connection to the app. The terminal is sandboxed and only provides access to this app container's filesystem.</p>
<a class="btn btn-primary pull-right" ng-href="{{ (app.installationState === 'installed' && (app.health === 'healthy' || app.debugMode)) ? '/terminal.html?id=' + app.id : '' }}" ng-disabled="app.installationState !== 'installed' || (app.health !== 'healthy' && !app.debugMode)" tooltip-class="long" tooltip-enable="app.installationState !== 'installed' || app.health !== 'healthy'" uib-tooltip="App is not running. If app is constantly restarting, Repair it first." target="_blank">Terminal</a>
<a class="btn btn-primary pull-right" ng-href="{{ '/logs.html?appId=' + app.id }}" target="_blank">Logs</a>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
<label class="control-label">Start / Stop</label>
<p>Apps can be stopped to conserve server resources. They will continue to be backed up but won't be updated.</p>
<button class="btn btn-primary pull-right" ng-class="{ 'btn-danger': !console.startButton }" ng-click="console.toggleRunState()" ng-disabled="app.taskId || app.error || console.busyRunState" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i ng-show="app.installationState === 'pending_start' || app.installationState === 'pending_stop'" class="fa fa-circle-notch fa-spin"></i>
{{ console.startButton ? 'Start App' : 'Stop App' }}
</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'repair'">
<div class="row">
<div class="col-md-12">
<label class="control-label">Crash Recovery</label>
<p>
If the app is not responding, try restarting the app. If the app is constantly restarting because of a broken plugin or misconfiguration,
place the app in recovery mode in order to access the console.
Use the following <a target="_blank" ng-href="{{ config.webServerOrigin }}/documentation/troubleshooting/#unresponsive-app">instructions</a>
to get the app running again.
</p>
<button class="btn btn-primary pull-right" ng-click="repair.pauseAppBegin()" ng-show="!app.debugMode" ng-disabled="repair.pauseBusy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">Enable Recovery Mode</button>
<button class="btn btn-primary pull-right" ng-click="repair.pauseAppDone()" ng-show="app.debugMode" ng-disabled="repair.pauseBusy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">Disable Recovery Mode</button>
<button class="btn btn-primary pull-right" ng-click="repair.restartApp()" ng-disabled="repair.restartBusy || app.error || app.taskId" tooltip-enable="app.error || app.taskId" uib-tooltip="{{ app.error ? 'App is in error state' : 'App is busy' }}">
<i ng-show="repair.restartBusy" class="fa fa-circle-notch fa-spin"></i>
Restart App
</button>
</div>
</div>
<hr/>
<div class="row">
<div class="col-md-12">
<label class="control-label">Task Error</label>
<p>If a configuration, update, restore or backup action resulted in an error, you can retry the task.</p>
<p ng-show="app.error">An error occurred during the <b>{{ app.error.installationState | taskName }}</b> operation: <span class="text-danger"><b>{{ app.error.reason + ': ' + app.error.message }}</b></span></p>
<button class="btn btn-primary pull-right" ng-click="repair.confirm()" ng-disabled="app.taskId || !app.error" tooltip-enable="app.taskId" uib-tooltip="App is busy">Retry {{ app.error.installationState | taskName }}</button>
</div>
</div>
</div>
<div class="card" ng-show="view === 'uninstall'">
<div class="row">
<div class="col-md-12">
<label class="control-label">Uninstall</label>
<p>This will uninstall the app immediately and remove all it's data. The site will be inaccessible.
</p>
<p>App backups are not removed and will be cleaned up based on the backup policy. You can resurrect this app from an existing
app backup using the following <a target="_blank" ng-href="{{ config.webServerOrigin }}/documentation/backups/##import-app-backup">instructions</a>.</p>
<button class="btn btn-danger pull-right" ng-click="uninstall.ask()">Uninstall</button>
</div>
</div>
</div>
</div>
</div>
</div>