This is working, from a configuration standpoint. However not all auth methods support this yet, so we hide it until that is done, otherwise it is just confusing
354 lines
24 KiB
HTML
354 lines
24 KiB
HTML
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length === 0">
|
|
<div class="col-lg-6 col-lg-offset-3" style="text-align: center;">
|
|
<br/><br/><br/><br/>
|
|
<h1><i class="fa fa-cloud-download fa-fw"></i> Your Cloudron does not have any apps installed yet!</h1>
|
|
<br/></br>
|
|
<h3>How about installing some? Checkout the <a href="#/appstore">App Store</a></h3>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal configure app -->
|
|
<div class="modal fade" id="appConfigureModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">Configure {{ appConfigure.app.manifest.title }}</h4>
|
|
</div>
|
|
<div class="modal-body">
|
|
<fieldset>
|
|
<form 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>{{appConfigure.app.accessRestriction.users[0]}}</b>.
|
|
</p>
|
|
</div>
|
|
<div class="form-group" ng-hide="true || 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>
|
|
<span ng-repeat="group in groups">
|
|
<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>
|
|
<!-- Not sure if oauthproxy makes any sense with singleuser apps, it certainly looks strange in the UI, so we hide it for now -->
|
|
<div class="form-group" ng-hide="appConfigure.app.manifest.singleUser">
|
|
<label class="control-label" for="oauthProxy">Website Visibility</label>
|
|
<select class="form-control" id="oauthProxy" ng-model="appConfigure.oauthProxy">
|
|
<option value="">Visible to all</option>
|
|
<option value="1">Visible only to Cloudron users</option>
|
|
</select>
|
|
</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="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.location }}{{ !appConfigure.app.location ? '' : (config.isCustomDomain ? '.' : '-') }}{{ config.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"/>
|
|
</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"><i class="fa fa-spinner fa-pulse" ng-show="appConfigure.busy"></i> Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal restore app -->
|
|
<div class="modal fade" id="appRestoreModal" tabindex="-1" role="dialog">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h4 class="modal-title">Really Restore {{ appRestore.app.location }}</h4>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p 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.location }}</h4>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>There was an error installing this app</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.location }}</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.location }}</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>Installed Applications</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
|
<div class="col-sm-1 grid-item" ng-repeat="app in installedApps">
|
|
<div style="background-color: white;" class="highlight grid-item-content">
|
|
<a ng-href="{{app | applicationLink}}" 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.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/>
|