823 lines
48 KiB
HTML
823 lines
48 KiB
HTML
<!-- TODO use this once we enable custom SSL certificate ui again -->
|
|
<!-- <div class="hide">
|
|
<label class="control-label" for="appConfigureCertificateInput" ng-show="appConfigure.domain.provider !== 'caas'">Certificate (optional)</label>
|
|
<div class="has-error text-center" ng-show="appConfigure.error.cert && appConfigure.domain.provider !== 'caas'">{{ appConfigure.error.cert }}</div>
|
|
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.certificate.$dirty && appConfigure.error.cert }" ng-show="appConfigure.domain.provider !== 'caas'">
|
|
<div class="input-group">
|
|
<input type="file" id="appConfigureCertificateFileInput" onchange="readCertificate()" style="display:none"/>
|
|
<input type="text" class="form-control" placeholder="Certificate" ng-model="appConfigure.certificateFileName" id="appConfigureCertificateInput" name="certificate" onclick="getElementById('appConfigureCertificateFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.keyFileName">
|
|
<span class="input-group-addon">
|
|
<i class="fa fa-upload" onclick="getElementById('appConfigureCertificateFileInput').click();"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.key.$dirty && appConfigure.error.cert }" ng-show="appConfigure.domain.provider !== 'caas'">
|
|
<div class="input-group">
|
|
<input type="file" id="appConfigureKeyFileInput" onchange="readKey()" style="display:none"/>
|
|
<input type="text" class="form-control" placeholder="Key" ng-model="appConfigure.keyFileName" id="appConfigureKeyInput" name="key" onclick="getElementById('appConfigureKeyFileInput').click();" style="cursor: pointer;" ng-required="appConfigure.certificateFileName">
|
|
<span class="input-group-addon">
|
|
<i class="fa fa-upload" onclick="getElementById('appConfigureKeyFileInput').click();"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div> -->
|
|
|
|
<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.version }}</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.busy"><i class="fa fa-circle-notch fa-spin" ng-show="repair.busy"></i> Repair</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>Recent 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>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<label class="checkbox-inline pull-left">
|
|
<input type="checkbox" ng-model="updates.skipBackup"><b>Skip backup</b>
|
|
</label>
|
|
|
|
<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="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.version }}</b></p>
|
|
<fieldset>
|
|
<form role="form" ng-submit="clone.submit()" autocomplete="off">
|
|
<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="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>
|
|
</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-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-apps-link"><i class="fas fa-arrow-left"></i> Back to My Apps</a>
|
|
|
|
<br/>
|
|
|
|
<div class="row">
|
|
<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:user }}</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">
|
|
<div class="col-sm-8 col-sm-offset-2" style="height: 10px;">
|
|
<div class="progress progress-striped active animateMeOpacity" ng-show="app.taskId" style="height: 10px;" uib-tooltip="{{ app.taskProgressMessage }}">
|
|
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.taskProgress }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row app-configure-links-container">
|
|
<div class="col-sm-2">
|
|
<div class="app-configure-links">
|
|
<div ng-click="setView('display')" ng-class="{ 'active': view === 'display', 'disabled': app.error }">Display</div>
|
|
<div ng-click="setView('location')" ng-class="{ 'active': view === 'location', 'disabled': app.error }">Location</div>
|
|
<div ng-click="setView('access')" ng-class="{ 'active': view === 'access', 'disabled': app.error }">Access Control</div>
|
|
<div ng-click="setView('resources')" ng-class="{ 'active': view === 'resources', 'disabled': app.error }">Resources</div>
|
|
<div ng-click="setView('security')" ng-class="{ 'active': view === 'security', 'disabled': app.error }">Security</div>
|
|
<div ng-click="setView('email')" ng-class="{ 'active': view === 'email', 'disabled': app.error }" ng-show="app.manifest.addons.sendmail || app.manifest.addons.recvmail">Email</div>
|
|
<div ng-click="setView('updates')" ng-class="{ 'active': view === 'updates', 'disabled': app.error }">Updates</div>
|
|
<div ng-click="setView('backups')" ng-class="{ 'active': view === 'backups', 'disabled': app.error }">Backups</div>
|
|
<div ng-click="setView('debug')" ng-class="{ 'active': view === 'debug' }">Debug</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">
|
|
<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()">
|
|
<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"/>
|
|
</form>
|
|
</fieldset>
|
|
</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="task-active-overlay" ng-show="app.taskId"></div>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<fieldset>
|
|
<form role="form" name="locationForm" ng-submit="location.submit()" autocomplete="off">
|
|
<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="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>
|
|
</form>
|
|
</fieldset>
|
|
</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"><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">
|
|
<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" 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="accessForm.$invalid || access.busy"/>
|
|
</div>
|
|
</form>
|
|
</fieldset>
|
|
</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.$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="task-active-overlay" ng-show="app.taskId"></div>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<fieldset>
|
|
<form role="form" name="resourcesForm" ng-submit="resources.submitMemoryLimit()" 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>
|
|
|
|
<input class="ng-hide" type="submit" ng-disabled="resources.memoryLimit === resources.currentMemoryLimit || resourcesForm.$invalid || resources.busy"/>
|
|
</form>
|
|
</fieldset>
|
|
</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"><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">
|
|
<label class="control-label" for="resourcesEnableDataDir">Custom Data Directory <sup><a ng-href="{{ config.webServerOrigin }}/documentation/storage/#moving-a-single-apps-data-directory-to-another-location" 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 mount an external disk and move this app's data there.
|
|
</p>
|
|
<fieldset>
|
|
<form role="form" name="resourcesDataDirForm" ng-submit="resources.submitDataDir()" autocomplete="off">
|
|
<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"/>
|
|
</form>
|
|
</fieldset>
|
|
</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"><i class="fa fa-circle-notch fa-spin" ng-show="resources.busyDataDir"></i> Move Data</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card" ng-show="view === 'email'">
|
|
<div class="task-active-overlay" ng-show="app.taskId"></div>
|
|
<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>
|
|
|
|
<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.$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="!emailForm.$dirty || email.busy"/>
|
|
</form>
|
|
</fieldset>
|
|
</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="!emailForm.$dirty || email.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">
|
|
<fieldset>
|
|
<form role="form" name="securityForm" ng-submit="security.submit()" autocomplete="off">
|
|
<div class="form-group">
|
|
<label class="control-label" style="width: 100%">Robots.txt <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 = 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"/>
|
|
</form>
|
|
</fieldset>
|
|
</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"><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 | installSuccess)">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">Create Backup</label>
|
|
<p>This creates a snapshot of the app at this instant.</p>
|
|
<button type="button" class="btn btn-primary pull-right" ng-click="backups.createBackup()" ng-disabled="app.taskId || backups.busyCreate"><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">Backups</label>
|
|
<p>You can use app backups to restore or clone this app.</p>
|
|
|
|
<!-- backup id copy helper -->
|
|
<input type="text" class="offscreen" aria-hidden="true" id="backupIdHelper" value="">
|
|
|
|
<table class="table table-hover" style="margin: 0;">
|
|
<thead>
|
|
<tr>
|
|
<th width="25px"> </th>
|
|
<th>Created</th>
|
|
<th>Version</th>
|
|
<th class="text-right" width="180px">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr ng-hide="backups.backups.length">
|
|
<td colspan="4" class="text-center">This app has no backups yet.</td>
|
|
</tr>
|
|
<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 }}">{{ backup.creationTime | prettyDate }}</div></td>
|
|
<td>{{ backup.version }}</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>
|
|
</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. If you disable automatic backups, be sure to manually create backups often.</p>
|
|
<p>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 === 'debug'">
|
|
<div class="row" ng-hide="true">
|
|
<div class="col-md-12">
|
|
<label class="control-label">Status</label>
|
|
<div>
|
|
Version
|
|
<span class="app-info-meta text-small">{{ app.manifest.title }} {{ app.upstreamVersion }} (Package <a ng-href="/#/appstore/{{app.manifest.id}}?version={{app.manifest.version}}">v{{ app.manifest.version }}</a>) </span>
|
|
<br/>
|
|
App ID <span class="app-info-meta text-small">{{ app.id }}</a> </span>
|
|
<br/>
|
|
Installed <span class="app-info-meta text-small">{{ app.creationTime | prettyDate }}</a> </span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<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 the app container's filesystem.</p>
|
|
<a class="btn btn-primary pull-right" ng-href="{{ '/terminal.html?id=' + app.id }}" 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>If the app is not responding, try restarting the app.</p>
|
|
<button class="btn btn-success pull-right" ng-class="{ 'btn-danger': app.runState === 'running' }" ng-click="debug.toggleRunState()" ng-disabled="app.taskId || app.error || debug.busyRunState"><i ng-show="debug.busyRunState" class="fa fa-circle-notch fa-spin"></i>
|
|
{{ app.runState === 'running' ? 'Stop App' : 'Start App' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<hr/>
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<label class="control-label">Repair</label>
|
|
<p>Use the repair button to retry failed operations or if the app is down.</p>
|
|
|
|
<p>If the app is not starting up because of a broken plugin or misconfiguration, use the <a ng-href="{{ '/terminal.html?id=' + app.id }}" target="_blank">Terminal</a>
|
|
and click 'Pause' to edit the app's filesystem.</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>
|
|
<!--<p ng-show="app.error" class="text-info"><b>{{ app.error | errorSuggestion }}</b></p>-->
|
|
<button class="btn btn-success pull-right" ng-click="repair.show()" ng-disabled="app.taskId">Repair</button>
|
|
|
|
<button class="btn btn-danger pull-right" ng-click="debug.stopAppTask(app.taskId)" ng-show="app.taskId">Cancel Current Task</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/#restoring-uninstalled-app">instructions</a>.</p>
|
|
<button class="btn btn-danger pull-right" ng-click="uninstall.ask()">Uninstall</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|