362 lines
24 KiB
HTML
362 lines
24 KiB
HTML
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && user.admin">
|
|
<div class="col-lg-6 col-lg-offset-3" style="text-align: center;">
|
|
<br/><br/><br/><br/>
|
|
<h1><i class="fa fa-cloud-download fa-fw"></i> Your Cloudron does not have any apps installed yet!</h1>
|
|
<br/></br>
|
|
<h3>How about installing some? Checkout the <a href="#/appstore">App Store</a></h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length === 0 && !user.admin">
|
|
<div class="col-lg-6 col-lg-offset-3" style="text-align: center;">
|
|
<br/><br/><br/><br/>
|
|
<h1>You don't have access to any apps on this Cloudron yet!</h1>
|
|
<br/></br>
|
|
<h3>Once you do, they will show up here.</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal configure app -->
|
|
<div class="modal fade" id="appConfigureModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">Configure {{ appConfigure.app.manifest.title }}</h4>
|
|
</div>
|
|
<div class="modal-body">
|
|
<fieldset>
|
|
<form role="form" name="appConfigureForm" ng-submit="doConfigure()" autocomplete="off">
|
|
<div class="has-error text-center" ng-show="appConfigure.error.other">{{ appConfigure.error.other }}</div>
|
|
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.location.$dirty && appConfigureForm.location.$invalid) || (!appConfigureForm.location.$dirty && appConfigure.error.location) }">
|
|
<label class="control-label" for="appConfigureLocationInput">Location {{ appConfigure.error.location }} </label>
|
|
<div class="input-group form-inline">
|
|
<input type="text" class="form-control" ng-model="appConfigure.location" id="appConfigureLocationInput" name="location" placeholder="Leave empty to use bare domain" autofocus>
|
|
<div class="input-group-addon">
|
|
{{ !appConfigure.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.fqdn }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="has-error text-center" ng-show="appConfigure.error.port">{{ appConfigure.error.port }}</div>
|
|
<div ng-repeat="(env, info) in appConfigure.portBindingsInfo">
|
|
<ng-form name="portInfo_form">
|
|
<div class="form-group" ng-class="{ 'has-error': (!appConfigureForm.itemName{{$index}}.$dirty && appConfigure.error.port) || (portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid) }">
|
|
<label class="control-label" for="appConfigurePortInput{{env}}"><input type="checkbox" ng-model="appConfigure.portBindingsEnabled[env]"> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
|
|
<input type="number" class="form-control" ng-model="appConfigure.portBindings[env]" ng-disabled="!appConfigure.portBindingsEnabled[env]" id="appConfigurePortInput{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
|
|
</div>
|
|
</ng-form>
|
|
</div>
|
|
<div class="form-group" ng-show="appConfigure.app.manifest.singleUser">
|
|
<label class="control-label">User</label>
|
|
<p>
|
|
This is a single user application.<br/><br/>
|
|
Access is granted to <b>{{ renderAccessRestrictionUser(appConfigure.app.accessRestriction.users[0]) }}</b>.
|
|
</p>
|
|
</div>
|
|
<div class="form-group" ng-hide="appConfigure.app.manifest.singleUser">
|
|
<label class="control-label">Access control</label>
|
|
<div class="radio">
|
|
<label>
|
|
<input type="radio" ng-model="appConfigure.accessRestrictionOption" value="">
|
|
Every Cloudron user
|
|
</label>
|
|
</div>
|
|
<div class="radio">
|
|
<label>
|
|
<input type="radio" ng-model="appConfigure.accessRestrictionOption" value="restricted">
|
|
Restrict to groups
|
|
</label>
|
|
</div>
|
|
<div class="has-error" ng-show="appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid()">Select at least one group</div>
|
|
<div>
|
|
<div>
|
|
<span ng-repeat="group in groups | ignoreAdminGroup">
|
|
<button class="btn btn-default" type="button" ng-disabled="appConfigure.accessRestrictionOption === ''" ng-click="appConfigureToggleGroup(group);" ng-class="{ 'btn-primary': (appConfigure.accessRestriction.groups && appConfigure.accessRestriction.groups.indexOf(group.id) !== -1) }">{{ group.name }}</button>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="form-group" ng-hide="true">
|
|
<label class="control-label" for="memoryUsage">Maximum Memory Usage: <b>{{ appConfigure.memoryUsage / 1024 / 1024 }} MB</b></label>
|
|
<br/>
|
|
<div style="padding: 0 10px;">
|
|
<slider id="memoryUsage" ng-model="appConfigure.memoryUsage" step="33554432" tooltip="hide" ticks="memoryTicks" ticks-snap-bounds="67108864"></slider>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="control-label" for="appConfigureAltDomainInput">Alternate external domain</label>
|
|
<br/>
|
|
<input type="text" class="form-control" ng-model="appConfigure.altDomain" id="appConfigureAltDomainInput" name="altDomain" placeholder="chat.example.com" autofocus>
|
|
</div>
|
|
|
|
<div class="hide">
|
|
<label class="control-label" for="appConfigureCertificateInput" ng-show="config.isCustomDomain">Certificate (optional)</label>
|
|
<div class="has-error text-center" ng-show="appConfigure.error.cert && config.isCustomDomain">{{ appConfigure.error.cert }}</div>
|
|
<div class="form-group" ng-class="{ 'has-error': !appConfigureForm.certificate.$dirty && appConfigure.error.cert }" ng-show="config.isCustomDomain">
|
|
<div class="input-group">
|
|
<input type="file" id="appConfigureCertificateFileInput" 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="config.isCustomDomain">
|
|
<div class="input-group">
|
|
<input type="file" id="appConfigureKeyFileInput" 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>
|
|
|
|
<a ng-show="!!appConfigure.app.manifest.configurePath" ng-href="https://{{ appConfigure.app.fqdn }}/{{ appConfigure.app.manifest.configurePath }}" target="_blank">Application Specific Settings</a>
|
|
<br/>
|
|
<br/>
|
|
<div class="form-group" ng-class="{ 'has-error': (appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password) }">
|
|
<label class="control-label" for="appConfigurePasswordInput">Provide your password to confirm this action</label>
|
|
<div class="control-label" ng-show="(appConfigureForm.password.$dirty && appConfigureForm.password.$invalid) || (!appConfigureForm.password.$dirty && appConfigure.error.password)">
|
|
<small ng-show=" appConfigureForm.password.$dirty && appConfigureForm.password.$invalid">Password required</small>
|
|
<small ng-show="!appConfigureForm.password.$dirty && appConfigure.error.password">Wrong password</small>
|
|
</div>
|
|
<input type="password" class="form-control" ng-model="appConfigure.password" id="appConfigurePasswordInput" name="password" required>
|
|
</div>
|
|
<input class="ng-hide" type="submit" ng-disabled="appConfigureForm.$invalid || busy || (appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid())"/>
|
|
</form>
|
|
</fieldset>
|
|
</div>
|
|
<div class="modal-footer ">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption !== '' && !appConfigure.isAccessRestrictionValid())"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal restore app -->
|
|
<div class="modal fade" id="appRestoreModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">Really Restore {{ appRestore.app.fqdn }}</h4>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p ng-show="appRestore.app.lastBackupId !== null">Restoring the app will lose all content generated since last backup of this app!</p>
|
|
<p ng-show="appRestore.app.lastBackupId === null">This app was never backed up. Restoring the app will lose all content!</p>
|
|
<fieldset>
|
|
<form role="form" name="appRestoreForm" ng-submit="doRestore()" autocomplete="off">
|
|
<div class="form-group" ng-class="{ 'has-error': (appRestoreForm.password.$dirty && appRestoreForm.password.$invalid) || (!appRestoreForm.password.$dirty && appRestore.error.password) }">
|
|
<label class="control-label" for="appRestorePasswordInput">Provide your password to confirm this action</label>
|
|
<div class="control-label" ng-show="(appRestoreForm.password.$dirty && appRestoreForm.password.$invalid) || (!appRestoreForm.password.$dirty && appRestore.error.password)">
|
|
<small ng-show=" appRestoreForm.password.$dirty && appRestoreForm.password.$invalid">Password required</small>
|
|
<small ng-show="!appRestoreForm.password.$dirty && appRestore.error.password">Wrong password</small>
|
|
</div>
|
|
<input type="password" class="form-control" ng-model="appRestore.password" id="appRestorePasswordInput" name="password" required autofocus>
|
|
</div>
|
|
|
|
<input class="ng-hide" type="submit" ng-disabled="appRestoreForm.$invalid || busy"/>
|
|
</form>
|
|
</fieldset>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-success" ng-click="doRestore()" ng-disabled="appRestoreForm.$invalid || appRestore.busy"><i class="fa fa-spinner fa-pulse" ng-show="appRestore.busy"></i> Restore</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal error app -->
|
|
<div class="modal fade" id="appErrorModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">{{ appError.app.fqdn }}</h4>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p><b>There was an error:</b></p>
|
|
<p>{{appError.app.installationProgress}}</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal uninstall app -->
|
|
<div class="modal fade" id="appUninstallModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">Really uninstall {{ appUninstall.app.fqdn }} ?</h4>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Deleting the app will also remove all content generated within this app!</p>
|
|
<fieldset>
|
|
<form role="form" name="appUninstallForm" ng-submit="doUninstall()" autocomplete="off">
|
|
<div class="form-group" ng-class="{ 'has-error': (appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) || (!appUninstallForm.password.$dirty && appUninstall.error.password) }">
|
|
<label class="control-label" for="appUninstallPasswordInput">Provide your password to confirm this action</label>
|
|
<div class="control-label" ng-show="(appUninstallForm.password.$dirty && appUninstallForm.password.$invalid) || (!appUninstallForm.password.$dirty && appUninstall.error.password)">
|
|
<small ng-show=" appUninstallForm.password.$dirty && appUninstallForm.password.$invalid">Password required</small>
|
|
<small ng-show="!appUninstallForm.password.$dirty && appUninstall.error.password">Wrong password</small>
|
|
</div>
|
|
<input type="password" class="form-control" ng-model="appUninstall.password" id="appUninstallPasswordInput" name="password" required autofocus>
|
|
</div>
|
|
|
|
<input class="ng-hide" type="submit" ng-disabled="appUninstallForm.$invalid || busy"/>
|
|
</form>
|
|
</fieldset>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-danger" ng-click="doUninstall()" ng-disabled="appUninstallForm.$invalid || appUninstall.busy"><i class="fa fa-spinner fa-pulse" ng-show="appUninstall.busy"></i> Uninstall</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal update app -->
|
|
<div class="modal fade" id="appUpdateModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">Update {{ appUpdate.app.fqdn }}</h4>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Recent Changes for new version <b>{{ appUpdate.manifest.version}}</b>:</p>
|
|
<pre>{{ appUpdate.manifest.changelog }}</pre>
|
|
<br/>
|
|
<fieldset>
|
|
<form role="form" name="appUpdateForm" ng-submit="doUpdate(appUpdateForm)" autocomplete="off">
|
|
<div ng-repeat="(env, info) in appUpdate.portBindingsInfo" ng-class="{ 'newPort': info.isNew }">
|
|
<ng-form name="portInfo_form">
|
|
<div class="form-group" ng-class="{ 'has-error': portInfo_form.itemName{{$index}}.$dirty && portInfo_form.itemName{{$index}}.$invalid }">
|
|
<label class="control-label" for="inputPortInfo{{env}}"><input type="checkbox" ng-model="appUpdate.portBindingsEnabled[env]"> <span ng-show="info.isNew">New - </span> {{ info.description }} ({{ HOST_PORT_MIN }} - {{ HOST_PORT_MAX }})</label>
|
|
<input type="number" class="form-control" ng-model="appUpdate.portBindings[env]" ng-disabled="!appUpdate.portBindingsEnabled[env]" id="inputPortInfo{{env}}" later-name="itemName{{$index}}" min="{{HOST_PORT_MIN}}" max="{{HOST_PORT_MAX}}" required>
|
|
</div>
|
|
</ng-form>
|
|
</div>
|
|
<div ng-repeat="(env, port) in appUpdate.obsoletePortBindings" class="obsoletePort">
|
|
<ng-form name="obsoletePortInfo_form">
|
|
<div class="form-group">
|
|
Obsolete -
|
|
<label class="control-label">{{ env }}</label>
|
|
<input type="number" class="form-control" ng-model="port" disabled>
|
|
</div>
|
|
</ng-form>
|
|
</div>
|
|
<div class="form-group" ng-class="{ 'has-error': (!appUpdateForm.password.$dirty && appUpdate.error.password) || (appUpdateForm.password.$dirty && appUpdateForm.password.$invalid) }">
|
|
<label class="control-label" for="inputUpdatePassword">Provide your password to confirm this action</label>
|
|
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" ng-maxlength="30" ng-minlength="8" required autofocus>
|
|
</div>
|
|
<input class="ng-hide" type="submit" ng-disabled="appUpdateForm.$invalid || busy"/>
|
|
</form>
|
|
</fieldset>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-danger" ng-click="doUpdate(appUpdateForm)" ng-disabled="appUpdateForm.$invalid || appUpdate.busy"><i class="fa fa-spinner fa-pulse" ng-show="appUpdate.busy"></i> Update</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function imageErrorHandler(elem) {
|
|
'use strict';
|
|
|
|
var appstoreIconUrl = elem.getAttribute('appstore-icon');
|
|
var fallbackIconUrl = elem.getAttribute('fallback-icon');
|
|
|
|
if (elem.src === appstoreIconUrl) {
|
|
elem.src = fallbackIconUrl;
|
|
elem.onerror = null; // avoid retry after default icon cannot be loaded
|
|
} else {
|
|
elem.src = appstoreIconUrl;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="content">
|
|
|
|
<br/>
|
|
|
|
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
|
<div class="col-lg-12">
|
|
<h1>Your Applications</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
|
<div class="col-sm-1 grid-item" ng-repeat="app in installedApps | orderBy:'location'">
|
|
<div style="background-color: white;" class="highlight grid-item-content">
|
|
<a ng-href="{{app | applicationLink}}" ng-click="(app | installError) === true && showError(app)" target="_blank">
|
|
<div class="grid-item-top">
|
|
<div class="row">
|
|
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
|
|
<img ng-src="{{app.iconUrl || 'img/appicon_fallback.png'}}" fallback-icon="img/appicon_fallback.png" appstore-icon="{{ app.iconUrlStore }}" onerror="imageErrorHandler(this)" class="app-icon"/>
|
|
</div>
|
|
</div>
|
|
<br/>
|
|
<div class="row">
|
|
<div class="col-xs-12 text-center">
|
|
<div class="grid-item-top-title">{{ app.altDomain || app.location || app.fqdn }}</div>
|
|
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
|
|
{{ app | installationStateLabel }}
|
|
</div>
|
|
<div ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
|
|
<div class="progress progress-striped active">
|
|
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid-item-bottom-mobile" ng-show="user.admin">
|
|
<div class="row">
|
|
<div class="col-xs-4 text-left">
|
|
<a href="" ng-click="showRestore(app)" ng-show="app.lastBackupId != null || (app | installError) === true">
|
|
<i class="fa fa-undo scale"></i>
|
|
</a>
|
|
|
|
<a href="" ng-click="showConfigure(app)" ng-show="(app | installSuccess) == true">
|
|
<i class="fa fa-wrench scale"></i>
|
|
</a>
|
|
</div>
|
|
<div class="col-xs-4 text-center"></div>
|
|
<div class="col-xs-4 text-right">
|
|
<a href="" ng-click="showUninstall(app)">
|
|
<i class="fa fa-remove scale"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid-item-bottom" ng-show="user.admin">
|
|
<div>
|
|
<a href="" ng-click="showUninstall(app)" title="Uninstall App"><i class="fa fa-remove scale"></i></a>
|
|
</div>
|
|
|
|
<div ng-show="app.lastBackupId !== null || (app | installError) === true">
|
|
<a href="" ng-click="showRestore(app)" title="Restore App"><i class="fa fa-undo scale"></i></a>
|
|
</div>
|
|
|
|
<div ng-show="(app | installSuccess) == true">
|
|
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-wrench scale"></i></a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
|
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
|
<a href="" ng-click="showUpdate(app)" title="Update Available"><i class="fa fa-asterisk fa-2x text-success scale"></i></a>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Offset the footer -->
|
|
<br/><br/>
|