2019-09-10 19:21:30 +02:00
'use strict' ;
/* global angular */
/* global $ */
/* global asyncSeries */
2019-09-20 01:55:45 +02:00
/* global asyncForEach */
2019-09-22 12:21:39 +02:00
/* global RSTATES */
/* global ISTATES */
2019-09-24 18:50:52 +02:00
/* global ERROR */
2020-03-26 00:16:23 +01:00
/* global moment */
2020-05-13 00:42:27 +02:00
/* global Chart */
2019-09-10 19:21:30 +02:00
2019-09-18 17:45:13 +02:00
angular . module ( 'Application' ) . controller ( 'AppController' , [ '$scope' , '$location' , '$timeout' , '$interval' , '$route' , '$routeParams' , 'Client' , function ( $scope , $location , $timeout , $interval , $route , $routeParams , Client ) {
2020-02-24 12:56:13 +01:00
Client . onReady ( function ( ) { if ( ! Client . getUserInfo ( ) . isAtLeastAdmin ) $location . path ( '/' ) ; } ) ;
2019-09-10 19:21:30 +02:00
2020-02-06 16:08:22 -08:00
// List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
$scope . s3Regions = [
{ name : 'Asia Pacific (Mumbai)' , value : 'ap-south-1' } ,
{ name : 'Asia Pacific (Osaka-Local)' , value : 'ap-northeast-3' } ,
{ name : 'Asia Pacific (Seoul)' , value : 'ap-northeast-2' } ,
{ name : 'Asia Pacific (Singapore)' , value : 'ap-southeast-1' } ,
{ name : 'Asia Pacific (Sydney)' , value : 'ap-southeast-2' } ,
{ name : 'Asia Pacific (Tokyo)' , value : 'ap-northeast-1' } ,
{ name : 'Canada (Central)' , value : 'ca-central-1' } ,
{ name : 'EU (Frankfurt)' , value : 'eu-central-1' } ,
{ name : 'EU (Ireland)' , value : 'eu-west-1' } ,
{ name : 'EU (London)' , value : 'eu-west-2' } ,
{ name : 'EU (Paris)' , value : 'eu-west-3' } ,
{ name : 'EU (Stockholm)' , value : 'eu-north-1' } ,
{ name : 'South America (São Paulo)' , value : 'sa-east-1' } ,
{ name : 'US East (N. Virginia)' , value : 'us-east-1' } ,
{ name : 'US East (Ohio)' , value : 'us-east-2' } ,
{ name : 'US West (N. California)' , value : 'us-west-1' } ,
{ name : 'US West (Oregon)' , value : 'us-west-2' } ,
] ;
$scope . wasabiRegions = [
{ name : 'EU Central 1' , value : 'https://s3.eu-central-1.wasabisys.com' } ,
{ name : 'US East 1' , value : 'https://s3.wasabisys.com' } ,
{ name : 'US West 1' , value : 'https://s3.us-west-1.wasabisys.com' }
] ;
$scope . doSpacesRegions = [
{ name : 'AMS3' , value : 'https://ams3.digitaloceanspaces.com' } ,
{ name : 'FRA1' , value : 'https://fra1.digitaloceanspaces.com' } ,
{ name : 'NYC3' , value : 'https://nyc3.digitaloceanspaces.com' } ,
{ name : 'SFO2' , value : 'https://sfo2.digitaloceanspaces.com' } ,
{ name : 'SGP1' , value : 'https://sgp1.digitaloceanspaces.com' }
] ;
$scope . exoscaleSosRegions = [
{ name : 'AT-VIE-1' , value : 'https://sos-at-vie-1.exo.io' } ,
{ name : 'CH-DK-2' , value : 'https://sos-ch-dk-2.exo.io' } ,
{ name : 'CH-GVA-2' , value : 'https://sos-ch-gva-2.exo.io' } ,
{ name : 'DE-FRA-1' , value : 'https://sos-de-fra-1.exo.io' } ,
] ;
// https://www.scaleway.com/docs/object-storage-feature/
$scope . scalewayRegions = [
{ name : 'FR-PAR' , value : 'https://s3.fr-par.scw.cloud' , region : 'fr-par' } , // default
{ name : 'NL-AMS' , value : 'https://s3.nl-ams.scw.cloud' , region : 'nl-ams' }
] ;
2020-03-05 11:24:42 -08:00
$scope . linodeRegions = [
{ name : 'Newark' , value : 'us-east-1.linodeobjects.com' , region : 'us-east-1' } , // default
{ name : 'Frankfurt' , value : 'eu-central-1.linodeobjects.com' , region : 'us-east-1' } ,
] ;
2020-04-29 12:54:19 -07:00
$scope . ovhRegions = [
{ name : 'Beauharnois (BHS)' , value : 'https://s3.bhs.cloud.ovh.net' , region : 'us-east-1' } , // default
{ name : 'Frankfurt (DE)' , value : 'https://s3.de.cloud.ovh.net' , region : 'us-east-1' } ,
{ name : 'Gravelines (GRA)' , value : 'https://s3.gra.cloud.ovh.net' , region : 'us-east-1' } ,
{ name : 'Strasbourg (SBG)' , value : 'https://s3.sbg.cloud.ovh.net' , region : 'us-east-1' } ,
{ name : 'London (UK)' , value : 'https://s3.uk.cloud.ovh.net' , region : 'us-east-1' } ,
{ name : 'Warsaw (WAW)' , value : 'https://s3.waw.cloud.ovh.net' , region : 'us-east-1' } ,
] ;
2020-02-06 16:08:22 -08:00
$scope . storageProvider = [
{ name : 'Amazon S3' , value : 's3' } ,
{ name : 'DigitalOcean Spaces' , value : 'digitalocean-spaces' } ,
{ name : 'Exoscale SOS' , value : 'exoscale-sos' } ,
{ name : 'Filesystem' , value : 'filesystem' } ,
{ name : 'Google Cloud Storage' , value : 'gcs' } ,
2020-03-05 11:24:42 -08:00
{ name : 'Linode Object Storage' , value : 'linode-objectstorage' } ,
2020-02-06 16:08:22 -08:00
{ name : 'Minio' , value : 'minio' } ,
2020-04-29 12:54:19 -07:00
{ name : 'OVH Object Storage' , value : 'ovh-objectstorage' } ,
2020-02-06 16:08:22 -08:00
{ name : 'Scaleway Object Storage' , value : 'scaleway-objectstorage' } ,
// { name: 'No-op (Only for testing)', value: 'noop' },
{ name : 'S3 API Compatible (v4)' , value : 's3-v4-compat' } ,
{ name : 'Wasabi' , value : 'wasabi' }
] ;
$scope . formats = [
{ name : 'Tarball (zipped)' , value : 'tgz' } ,
{ name : 'rsync' , value : 'rsync' }
] ;
2019-09-18 17:45:13 +02:00
// Avoid full reload on path change
// https://stackoverflow.com/a/22614334
// reloadOnUrl: false in $routeProvider did not work!
var lastRoute = $route . current ;
$scope . $on ( '$locationChangeSuccess' , function ( /* event */ ) {
if ( lastRoute . $$route . originalPath === $route . current . $$route . originalPath ) {
$route . current = lastRoute ;
}
} ) ;
var appId = $routeParams . appId ;
if ( ! appId ) return $location . path ( '/apps' ) ;
2019-09-10 19:21:30 +02:00
2019-09-18 17:45:13 +02:00
$scope . view = '' ;
2019-09-10 19:21:30 +02:00
$scope . app = null ;
$scope . config = Client . getConfig ( ) ;
$scope . user = Client . getUserInfo ( ) ;
$scope . domains = [ ] ;
$scope . groups = [ ] ;
$scope . users = [ ] ;
2019-09-19 18:41:03 +02:00
$scope . HOST _PORT _MIN = 1024 ;
$scope . HOST _PORT _MAX = 65535 ;
$scope . ROBOTS _DISABLE _INDEXING _TEMPLATE = '# Disable search engine indexing\n\nUser-agent: *\nDisallow: /' ;
2019-09-10 19:21:30 +02:00
2019-12-20 17:05:45 -08:00
$scope . setView = function ( view , skipViewShow ) {
2019-09-17 14:52:22 +02:00
if ( $scope . view === view ) return ;
2019-09-18 17:45:13 +02:00
$route . updateParams ( { view : view } ) ;
2019-12-20 17:05:45 -08:00
if ( ! skipViewShow ) $scope [ view ] . show ( ) ;
2019-09-17 14:52:22 +02:00
$scope . view = view ;
} ;
2020-03-26 00:19:06 +01:00
$scope . stopAppTask = function ( taskId ) {
Client . stopTask ( taskId , function ( error ) {
// we can ignore a call trying to cancel an already done task
if ( error && error . statusCode !== 409 ) Client . error ( error ) ;
} ) ;
} ,
2019-09-27 19:43:03 +02:00
$scope . postInstallMessage = {
confirmed : false ,
openApp : false ,
show : function ( openApp ) {
$scope . postInstallMessage . confirmed = false ;
$scope . postInstallMessage . openApp = ! ! openApp ;
if ( ! $scope . app . manifest . postInstallMessage ) return ;
$ ( '#postInstallModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
if ( ! $scope . postInstallMessage . confirmed ) return ;
$scope . app . pendingPostInstallConfirmation = false ;
delete localStorage [ 'confirmPostInstall_' + $scope . app . id ] ;
$ ( '#postInstallModal' ) . modal ( 'hide' ) ;
}
2019-09-26 20:48:04 +02:00
} ;
2019-09-10 19:21:30 +02:00
$scope . display = {
busy : false ,
error : { } ,
success : false ,
tags : '' ,
label : '' ,
icon : { data : null } ,
iconUrl : function ( ) {
if ( ! $scope . app ) return '' ;
if ( $scope . display . icon . data === '__original__' ) { // user clicked reset
return $scope . app . iconUrl + '&original=true' ;
} else if ( $scope . display . icon . data ) { // user uploaded icon
return $scope . display . icon . data ;
} else { // current icon
return $scope . app . iconUrl ;
}
} ,
resetCustomIcon : function ( ) {
$scope . display . icon . data = '__original__' ;
} ,
showCustomIconSelector : function ( ) {
$ ( '#iconFileInput' ) . click ( ) ;
} ,
show : function ( ) {
var app = $scope . app ;
2019-09-19 18:31:11 +02:00
$scope . display . error = { } ;
2019-09-10 19:21:30 +02:00
// translate for tag-input
2020-03-28 16:46:06 -07:00
$scope . display . tags = app . tags ? app . tags . join ( ' ' ) : '' ;
2019-09-10 19:21:30 +02:00
$scope . display . label = $scope . app . label || '' ;
$scope . display . icon = { data : null } ;
} ,
submit : function ( ) {
$scope . display . busy = true ;
$scope . display . error = { } ;
2019-10-11 15:18:48 -07:00
function done ( error ) {
if ( error ) Client . error ( error ) ;
2019-12-20 19:09:17 -08:00
$scope . displayForm . $setPristine ( ) ;
$scope . display . success = true ;
2019-09-17 14:49:26 +02:00
2019-12-20 19:09:17 -08:00
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) Client . error ( error ) ;
$scope . display . show ( ) ; // "refresh" view with latest data
$timeout ( function ( ) { $scope . display . busy = false ; } , 1000 ) ;
} ) ;
2019-09-12 16:28:21 +02:00
}
2019-09-27 13:38:30 -07:00
var NOOP = function ( next ) { return next ( ) ; } ;
var configureLabel = $scope . display . label === $scope . app . label ? NOOP : Client . configureApp . bind ( null , $scope . app . id , 'label' , { label : $scope . display . label } ) ;
configureLabel ( function ( error ) {
2019-10-11 15:18:48 -07:00
if ( error ) return done ( error ) ;
2019-09-10 19:21:30 +02:00
2020-03-28 16:46:06 -07:00
var tags = $scope . display . tags . split ( ' ' ) . map ( function ( t ) { return t . trim ( ) ; } ) . filter ( function ( t ) { return ! ! t ; } ) ;
2019-09-10 19:21:30 +02:00
2019-09-27 13:38:30 -07:00
var configureTags = angular . equals ( tags , $scope . app . tags ) ? NOOP : Client . configureApp . bind ( null , $scope . app . id , 'tags' , { tags : tags } ) ;
configureTags ( function ( error ) {
2019-10-11 15:18:48 -07:00
if ( error ) return done ( error ) ;
2019-09-10 19:21:30 +02:00
// skip if icon is unchanged
2019-09-12 16:28:21 +02:00
if ( $scope . display . icon . data === null ) return done ( ) ;
2019-09-10 19:21:30 +02:00
var icon ;
if ( $scope . display . icon . data === '__original__' ) { // user reset the icon
icon = '' ;
} else if ( $scope . display . icon . data ) { // user loaded custom icon
icon = $scope . display . icon . data . replace ( /^data:image\/[a-z]+;base64,/ , '' ) ;
}
Client . configureApp ( $scope . app . id , 'icon' , { icon : icon } , function ( error ) {
2019-10-11 15:18:48 -07:00
if ( error ) return done ( error ) ;
2019-09-12 16:28:21 +02:00
done ( ) ;
2019-09-10 19:21:30 +02:00
} ) ;
} ) ;
} ) ;
}
} ;
$scope . location = {
busy : false ,
error : { } ,
2019-09-20 01:55:45 +02:00
domainCollisions : [ ] ,
2019-09-10 19:21:30 +02:00
domain : null ,
location : '' ,
alternateDomains : [ ] ,
portBindings : { } ,
portBindingsEnabled : { } ,
portBindingsInfo : { } ,
addAlternateDomain : function ( event ) {
event . preventDefault ( ) ;
$scope . location . alternateDomains . push ( {
domain : $scope . domains [ 0 ] ,
subdomain : ''
} ) ;
} ,
delAlternateDomain : function ( event , index ) {
event . preventDefault ( ) ;
$scope . location . alternateDomains . splice ( index , 1 ) ;
} ,
show : function ( ) {
var app = $scope . app ;
2019-09-19 18:31:11 +02:00
$scope . location . error = { } ;
2019-09-20 01:55:45 +02:00
$scope . location . domainCollisions = [ ] ;
2019-09-10 19:21:30 +02:00
$scope . location . location = app . location ;
$scope . location . domain = $scope . domains . filter ( function ( d ) { return d . domain === app . domain ; } ) [ 0 ] ;
$scope . location . portBindingsInfo = angular . extend ( { } , app . manifest . tcpPorts , app . manifest . udpPorts ) ; // Portbinding map only for information
$scope . location . alternateDomains = app . alternateDomains . map ( function ( a ) { return { subdomain : a . subdomain , domain : $scope . domains . filter ( function ( d ) { return d . domain === a . domain ; } ) [ 0 ] } ; } ) ;
// fill the portBinding structures. There might be holes in the app.portBindings, which signalizes a disabled port
for ( var env in $scope . location . portBindingsInfo ) {
if ( app . portBindings && app . portBindings [ env ] ) {
$scope . location . portBindings [ env ] = app . portBindings [ env ] ;
$scope . location . portBindingsEnabled [ env ] = true ;
} else {
$scope . location . portBindings [ env ] = $scope . location . portBindingsInfo [ env ] . defaultValue || 0 ;
$scope . location . portBindingsEnabled [ env ] = false ;
}
}
} ,
2019-09-20 01:55:45 +02:00
submit : function ( overwriteDns ) {
$ ( '#domainCollisionsModal' ) . modal ( 'hide' ) ;
2019-09-10 19:21:30 +02:00
$scope . location . busy = true ;
$scope . location . error = { } ;
2019-09-20 01:55:45 +02:00
$scope . location . domainCollisions = [ ] ;
2019-09-10 19:21:30 +02:00
// only use enabled ports from portBindings
var portBindings = { } ;
for ( var env in $scope . location . portBindings ) {
if ( $scope . location . portBindingsEnabled [ env ] ) {
portBindings [ env ] = $scope . location . portBindings [ env ] ;
}
}
var data = {
2019-09-20 01:55:45 +02:00
overwriteDns : ! ! overwriteDns ,
2019-09-10 19:21:30 +02:00
location : $scope . location . location ,
domain : $scope . location . domain . domain ,
portBindings : portBindings ,
alternateDomains : $scope . location . alternateDomains . map ( function ( a ) { return { subdomain : a . subdomain , domain : a . domain . domain } ; } )
} ;
2019-09-20 01:55:45 +02:00
// pre-flight only for changed domains
var domains = [ ] ;
if ( $scope . app . domain !== data . domain || $scope . app . location !== data . location ) domains . push ( { subdomain : data . location , domain : data . domain } ) ;
data . alternateDomains . forEach ( function ( a ) {
if ( $scope . app . alternateDomains . some ( function ( d ) { return d . domain === a . domain && d . subdomain === a . subdomain ; } ) ) return ;
domains . push ( { subdomain : a . subdomain , domain : a . domain } ) ;
} ) ;
asyncForEach ( domains , function ( domain , callback ) {
if ( overwriteDns ) return callback ( ) ;
2019-09-24 00:04:31 -07:00
Client . checkDNSRecords ( domain . domain , domain . subdomain , function ( error , result ) {
2019-09-23 23:47:33 +02:00
if ( error ) return callback ( error ) ;
if ( result . error ) {
2019-09-23 19:23:00 +02:00
if ( data . domain === domain . domain && data . location === domain . subdomain ) {
2019-09-23 23:47:33 +02:00
$scope . location . error . location = domain . domain + ' ' + result . error . message ;
2019-09-23 19:23:00 +02:00
} else {
2019-09-23 23:47:33 +02:00
$scope . location . error . alternateDomains = domain . domain + ' ' + result . error . message ;
2019-09-23 19:23:00 +02:00
}
$scope . location . busy = false ;
return ;
}
2019-09-20 01:55:45 +02:00
2019-09-23 23:47:33 +02:00
if ( result . needsOverwrite ) $scope . location . domainCollisions . push ( domain ) ;
2019-09-20 01:55:45 +02:00
callback ( ) ;
} ) ;
} , function ( error ) {
if ( error ) {
$scope . location . busy = false ;
return Client . error ( error ) ;
}
2019-09-16 19:58:15 +02:00
2019-09-20 01:55:45 +02:00
if ( $scope . location . domainCollisions . length ) {
2019-09-16 19:58:15 +02:00
$scope . location . busy = false ;
2019-09-20 01:55:45 +02:00
return $ ( '#domainCollisionsModal' ) . modal ( 'show' ) ;
2019-09-16 19:58:15 +02:00
}
2019-09-10 19:21:30 +02:00
2019-09-20 01:55:45 +02:00
Client . configureApp ( $scope . app . id , 'location' , data , function ( error ) {
if ( error && ( error . statusCode === 409 || error . statusCode === 400 ) ) {
2019-09-27 14:42:37 -07:00
if ( ( error . subdomain && error . domain ) || error . field === 'location' ) {
if ( data . domain === error . domain && data . location === error . subdomain ) { // the primary
$scope . location . error . location = error . message ;
$scope . locationForm . $setPristine ( ) ;
} else {
$scope . location . error . alternateDomains = error . message ;
}
} else if ( error . portName || error . field === 'portBindings' ) {
$scope . location . error . port = error . message ;
2019-09-20 01:55:45 +02:00
}
$scope . location . busy = false ;
return ;
}
if ( error ) return Client . error ( error ) ;
2019-12-20 19:09:17 -08:00
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-09-11 21:24:25 +02:00
2019-12-20 19:09:17 -08:00
$scope . locationForm . $setPristine ( ) ;
$timeout ( function ( ) { $scope . location . busy = false ; } , 1000 ) ;
} ) ;
2019-09-20 01:55:45 +02:00
} ) ;
2019-09-10 19:21:30 +02:00
} ) ;
}
} ;
$scope . access = {
busy : false ,
error : { } ,
success : false ,
ftp : false ,
ssoAuth : false ,
accessRestrictionOption : 'any' ,
accessRestriction : { users : [ ] , groups : [ ] } ,
isAccessRestrictionValid : function ( ) {
var tmp = $scope . access . accessRestriction ;
return ! ! ( tmp . users . length || tmp . groups . length ) ;
} ,
show : function ( ) {
var app = $scope . app ;
2019-09-19 18:31:11 +02:00
$scope . access . error = { } ;
2019-09-10 19:21:30 +02:00
$scope . access . ftp = app . manifest . addons . localstorage && app . manifest . addons . localstorage . ftp ;
$scope . access . ssoAuth = ( app . manifest . addons [ 'ldap' ] || app . manifest . addons [ 'oauth' ] ) && app . sso ;
$scope . access . accessRestrictionOption = app . accessRestriction ? 'groups' : 'any' ;
$scope . access . accessRestriction = { users : [ ] , groups : [ ] } ;
if ( app . accessRestriction ) {
var userSet = { } ;
app . accessRestriction . users . forEach ( function ( uid ) { userSet [ uid ] = true ; } ) ;
$scope . users . forEach ( function ( u ) { if ( userSet [ u . id ] === true ) $scope . access . accessRestriction . users . push ( u ) ; } ) ;
var groupSet = { } ;
app . accessRestriction . groups . forEach ( function ( gid ) { groupSet [ gid ] = true ; } ) ;
$scope . groups . forEach ( function ( g ) { if ( groupSet [ g . id ] === true ) $scope . access . accessRestriction . groups . push ( g ) ; } ) ;
}
} ,
submit : function ( ) {
$scope . access . busy = true ;
$scope . access . error = { } ;
var accessRestriction = null ;
if ( $scope . access . accessRestrictionOption === 'groups' ) {
accessRestriction = { users : [ ] , groups : [ ] } ;
accessRestriction . users = $scope . access . accessRestriction . users . map ( function ( u ) { return u . id ; } ) ;
accessRestriction . groups = $scope . access . accessRestriction . groups . map ( function ( g ) { return g . id ; } ) ;
}
Client . configureApp ( $scope . app . id , 'access_restriction' , { accessRestriction : accessRestriction } , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-09-17 14:49:26 +02:00
$timeout ( function ( ) {
$scope . access . success = true ;
$scope . access . busy = false ;
} , 1000 ) ;
2019-09-10 19:21:30 +02:00
} ) ;
}
} ;
$scope . resources = {
error : { } ,
2020-04-29 22:18:44 -07:00
busy : false ,
2019-09-13 11:05:34 +02:00
currentMemoryLimit : 0 ,
2019-09-10 19:21:30 +02:00
memoryLimit : 0 ,
memoryTicks : [ ] ,
2020-01-28 22:05:06 -08:00
2020-04-29 22:18:44 -07:00
busyCpuShares : false ,
2020-01-28 22:05:06 -08:00
currentCpuShares : 0 ,
cpuShares : 0 ,
2020-04-29 22:18:44 -07:00
busyDataDir : false ,
2019-09-10 19:21:30 +02:00
dataDir : null ,
2020-04-29 22:18:44 -07:00
busyBinds : false ,
binds : [ ] ,
2019-09-10 19:21:30 +02:00
show : function ( ) {
var app = $scope . app ;
2019-09-19 18:31:11 +02:00
$scope . resources . error = { } ;
2019-09-13 11:05:34 +02:00
$scope . resources . currentMemoryLimit = app . memoryLimit || app . manifest . memoryLimit || ( 256 * 1024 * 1024 ) ;
$scope . resources . memoryLimit = $scope . resources . currentMemoryLimit ;
2020-01-28 22:05:06 -08:00
$scope . resources . currentCpuShares = $scope . resources . cpuShares = app . cpuShares ;
2019-09-10 19:21:30 +02:00
$scope . resources . dataDir = app . dataDir ;
2020-04-29 22:18:44 -07:00
$scope . resources . binds = [ ] ;
Object . keys ( app . binds ) . forEach ( function ( name ) {
$scope . resources . binds . push ( { name : name , hostPath : app . binds [ name ] . hostPath , readOnly : app . binds [ name ] . readOnly } ) ;
} ) ;
2019-09-10 19:21:30 +02:00
2019-12-12 12:13:06 +01:00
Client . memory ( function ( error , memory ) {
if ( error ) return console . error ( error ) ;
// create ticks starting from manifest memory limit. the memory limit here is currently split into ram+swap (and thus *2 below)
// TODO: the *2 will overallocate since 4GB is max swap that cloudron itself allocates
$scope . resources . memoryTicks = [ ] ;
var npow2 = Math . pow ( 2 , Math . ceil ( Math . log ( memory . memory ) / Math . log ( 2 ) ) ) ;
for ( var i = 256 ; i <= ( npow2 * 2 / 1024 / 1024 ) ; i *= 2 ) {
if ( i >= ( app . manifest . memoryLimit / 1024 / 1024 || 0 ) ) $scope . resources . memoryTicks . push ( i * 1024 * 1024 ) ;
}
if ( app . manifest . memoryLimit && $scope . resources . memoryTicks [ 0 ] !== app . manifest . memoryLimit ) {
$scope . resources . memoryTicks . unshift ( app . manifest . memoryLimit ) ;
}
} ) ;
2019-09-10 19:21:30 +02:00
} ,
2019-09-18 17:12:10 +02:00
submitMemoryLimit : function ( ) {
2019-09-10 19:21:30 +02:00
$scope . resources . busy = true ;
$scope . resources . error = { } ;
2019-09-11 21:24:25 +02:00
var memoryLimit = $scope . resources . memoryLimit === $scope . resources . memoryTicks [ 0 ] ? 0 : $scope . resources . memoryLimit ;
2019-09-10 19:21:30 +02:00
Client . configureApp ( $scope . app . id , 'memory_limit' , { memoryLimit : memoryLimit } , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-09-13 11:12:11 +02:00
$scope . resources . currentMemoryLimit = $scope . resources . memoryLimit ;
2019-09-10 19:21:30 +02:00
2019-12-20 19:09:17 -08:00
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
$timeout ( function ( ) { $scope . resources . busy = false ; } , 1000 ) ;
} ) ;
2019-09-18 17:12:10 +02:00
} ) ;
2020-01-28 22:05:06 -08:00
} ,
submitCpuShares : function ( ) {
$scope . resources . busyCpuShares = true ;
$scope . resources . error = { } ;
Client . configureApp ( $scope . app . id , 'cpu_shares' , { cpuShares : $scope . resources . cpuShares } , function ( error ) {
if ( error ) return Client . error ( error ) ;
$scope . resources . currentCpuShares = $scope . resources . cpuShares ;
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
$timeout ( function ( ) { $scope . resources . busyCpuShares = false ; } , 1000 ) ;
} ) ;
} ) ;
2019-09-18 17:12:10 +02:00
} ,
submitDataDir : function ( ) {
$scope . resources . busyDataDir = true ;
$scope . resources . error = { } ;
2019-09-20 01:22:10 +02:00
Client . configureApp ( $scope . app . id , 'data_dir' , { dataDir : $scope . resources . dataDir || null } , function ( error ) {
2019-09-18 17:12:10 +02:00
if ( error && error . statusCode === 400 ) {
$scope . resources . error . dataDir = error . message ;
$scope . resources . busyDataDir = false ;
return ;
}
if ( error ) return Client . error ( error ) ;
$scope . resourcesDataDirForm . $setPristine ( ) ;
2019-12-20 19:09:17 -08:00
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
$timeout ( function ( ) { $scope . resources . busyDataDir = false ; } , 1000 ) ;
} ) ;
2019-09-10 19:21:30 +02:00
} ) ;
2020-04-29 22:18:44 -07:00
} ,
addBind : function ( event ) {
event . preventDefault ( ) ;
$scope . resources . binds . push ( {
hostPath : '' ,
name : '' ,
readOnly : true
} ) ;
} ,
delBind : function ( event , index ) {
event . preventDefault ( ) ;
$scope . resources . binds . splice ( index , 1 ) ;
} ,
submitBinds : function ( ) {
$scope . resources . busyBinds = true ;
$scope . resources . error = { } ;
var binds = { } ;
$scope . resources . binds . forEach ( function ( bind ) {
binds [ bind . name ] = { hostPath : bind . hostPath , readOnly : bind . readOnly } ;
} ) ;
Client . configureApp ( $scope . app . id , 'binds' , { binds : binds } , function ( error ) {
if ( error && error . statusCode === 400 ) {
$scope . resources . error . binds = error . message ;
$scope . resources . busyBinds = false ;
return ;
}
if ( error ) return Client . error ( error ) ;
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
$timeout ( function ( ) { $scope . resources . busyBinds = false ; } , 1000 ) ;
} ) ;
} ) ;
2019-09-10 19:21:30 +02:00
}
} ;
2020-05-13 00:42:27 +02:00
$scope . graphs = {
error : { } ,
period : 6 ,
periodLabel : '6 hours' ,
memoryChart : null ,
diskChart : null ,
setPeriod : function ( hours , label ) {
$scope . graphs . period = hours ;
$scope . graphs . periodLabel = label ;
$scope . graphs . show ( ) ;
} ,
show : function ( ) {
2020-05-13 01:12:13 +02:00
// both in minutes
var timePeriod = $scope . graphs . period * 60 ;
var timeBucketSize = $scope . graphs . period > 24 ? ( 6 * 60 ) : 5 ;
2020-05-13 00:42:27 +02:00
function fillGraph ( canvasId , data , label , chartPropertyName ) {
// translate the data from bytes to MB
var datapoints = data . datapoints . map ( function ( d ) { return parseInt ( ( d [ 0 ] / 1024 / 1024 ) . toFixed ( 2 ) ) ; } ) ;
var labels = datapoints . map ( function ( d , index ) {
var dateTime = new Date ( Date . now ( ) - ( ( timePeriod - ( index * timeBucketSize ) ) * 60 * 1000 ) ) ;
return ( '0' + dateTime . getHours ( ) ) . slice ( - 2 ) + ':00' ;
} ) ;
var data = {
labels : labels ,
datasets : [ {
label : label ,
2020-05-13 01:12:13 +02:00
backgroundColor : '#82C4F844' ,
2020-05-13 00:42:27 +02:00
borderColor : '#2196F3' ,
2020-05-13 01:12:13 +02:00
borderWidth : 1 ,
radius : 2 ,
2020-05-13 00:42:27 +02:00
pointBackgroundColor : 'rgba(151,187,205,1)' ,
pointBorderColor : '#2196F3' ,
pointHoverBackgroundColor : '#82C4F8' ,
pointHoverBorderColor : '#82C4F8' ,
data : datapoints
} ]
} ;
var options = {
2020-05-13 01:12:13 +02:00
maintainAspectRatio : true ,
aspectRatio : 2.5 ,
2020-05-13 00:42:27 +02:00
legend : {
display : false
} ,
tooltips : {
intersect : false
} ,
scales : {
yAxes : [ {
ticks : {
min : 0 ,
beginAtZero : true
}
} ]
}
} ;
var ctx = $ ( canvasId ) . get ( 0 ) . getContext ( '2d' ) ;
if ( $scope . graphs [ chartPropertyName ] ) $scope . graphs [ chartPropertyName ] . destroy ( ) ;
$scope . graphs [ chartPropertyName ] = new Chart ( ctx , { type : 'line' , data : data , options : options } ) ;
}
var memoryQuery = 'summarize(collectd.localhost.table-' + appId + '-memory.gauge-rss, "' + timeBucketSize + 'min", "avg")' ;
2020-05-13 22:53:51 +02:00
var diskQuery = 'summarize(collectd.localhost.du-' + appId + '.capacity-usage, "' + timeBucketSize + 'min", "avg")' ;
2020-05-13 00:42:27 +02:00
2020-05-13 22:53:51 +02:00
Client . graphs ( [ memoryQuery , diskQuery ] , '-' + timePeriod + 'min' , { } , function ( error , result ) {
2020-05-13 00:42:27 +02:00
if ( error ) return console . error ( error ) ;
fillGraph ( '#graphsMemoryChart' , result [ 0 ] , 'Memory' , 'memoryChart' ) ;
fillGraph ( '#graphsDiskChart' , result [ 1 ] , 'Disk' , 'diskChart' ) ;
} ) ;
}
} ;
2019-09-10 19:21:30 +02:00
$scope . email = {
busy : false ,
error : { } ,
mailboxName : '' ,
2019-11-14 22:28:23 -08:00
mailboxDomain : '' ,
2020-02-27 16:04:11 +01:00
currentMailboxName : '' ,
currentMailboxDomainName : '' ,
2019-09-10 19:21:30 +02:00
show : function ( ) {
var app = $scope . app ;
2019-09-19 18:00:18 -07:00
$scope . emailForm . $setPristine ( ) ;
2019-09-19 18:31:11 +02:00
$scope . email . error = { } ;
2019-09-10 19:21:30 +02:00
$scope . email . mailboxName = app . mailboxName || '' ;
2019-11-14 22:28:23 -08:00
$scope . email . mailboxDomain = $scope . domains . filter ( function ( d ) { return d . domain === app . mailboxDomain ; } ) [ 0 ] ;
2020-02-27 16:04:11 +01:00
$scope . email . currentMailboxName = app . mailboxName || '' ;
$scope . email . currentMailboxDomainName = $scope . email . mailboxDomain . domain ;
2019-09-10 19:21:30 +02:00
} ,
submit : function ( ) {
2019-09-17 15:09:39 +02:00
$scope . email . error = { } ;
2019-09-10 19:21:30 +02:00
$scope . email . busy = true ;
2019-09-11 21:24:25 +02:00
2019-11-14 22:28:23 -08:00
Client . configureApp ( $scope . app . id , 'mailbox' , { mailboxName : $scope . email . mailboxName || null , mailboxDomain : $scope . email . mailboxDomain . domain } , function ( error ) {
2019-09-17 15:09:39 +02:00
if ( error && error . statusCode === 400 ) {
$scope . email . busy = false ;
$scope . email . error . mailboxName = error . message ;
$scope . emailForm . $setPristine ( ) ;
return ;
}
if ( error ) return Client . error ( error ) ;
2019-09-11 21:24:25 +02:00
2019-09-19 18:00:18 -07:00
$scope . emailForm . $setPristine ( ) ;
2019-12-16 16:08:49 -08:00
refreshApp ( $scope . app . id , function ( error ) {
2019-12-20 19:09:17 -08:00
if ( error ) return Client . error ( error ) ;
2019-09-19 18:00:18 -07:00
// when the mailboxName is 'reset', this will fill it up with the default again
$scope . email . mailboxName = $scope . app . mailboxName || '' ;
2019-11-14 22:28:23 -08:00
$scope . email . mailboxDomain = $scope . domains . filter ( function ( d ) { return d . domain === $scope . app . mailboxDomain ; } ) [ 0 ] ;
2020-02-27 16:04:11 +01:00
$scope . email . currentMailboxName = $scope . app . mailboxName || '' ;
$scope . email . currentMailboxDomainName = $scope . email . mailboxDomain . domain ;
2019-12-20 19:09:17 -08:00
$timeout ( function ( ) { $scope . email . busy = false ; } , 1000 ) ;
2019-09-19 18:00:18 -07:00
} ) ;
2019-09-17 15:09:39 +02:00
} ) ;
2019-09-10 19:21:30 +02:00
}
} ;
$scope . security = {
busy : false ,
error : { } ,
success : false ,
robotsTxt : '' ,
2019-10-14 16:50:15 -07:00
csp : '' ,
2019-09-10 19:21:30 +02:00
show : function ( ) {
2019-09-19 18:31:11 +02:00
$scope . security . error = { } ;
2019-10-14 15:20:48 -07:00
$scope . security . robotsTxt = $scope . app . reverseProxyConfig . robotsTxt || '' ;
2019-10-14 16:50:15 -07:00
$scope . security . csp = $scope . app . reverseProxyConfig . csp || '' ;
2019-09-10 19:21:30 +02:00
} ,
submit : function ( ) {
$scope . security . busy = true ;
$scope . security . error = { } ;
2019-10-14 15:20:48 -07:00
var reverseProxyConfig = {
2019-10-14 16:50:15 -07:00
robotsTxt : $scope . security . robotsTxt || null , // empty string resets
csp : $scope . security . csp || null // empty string resets
2019-10-14 15:20:48 -07:00
} ;
Client . configureApp ( $scope . app . id , 'reverse_proxy' , reverseProxyConfig , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-09-17 14:49:26 +02:00
$timeout ( function ( ) {
$scope . security . success = true ;
$scope . security . busy = false ;
} , 1000 ) ;
2019-09-10 19:21:30 +02:00
} ) ;
}
} ;
$scope . updates = {
busy : false ,
2019-09-17 16:16:48 +02:00
busyCheck : false ,
2019-09-17 17:14:40 +02:00
busyUpdate : false ,
2019-09-26 20:10:25 -07:00
skipBackup : false ,
2019-09-10 19:21:30 +02:00
enableAutomaticUpdate : false ,
show : function ( ) {
var app = $scope . app ;
$scope . updates . enableAutomaticUpdate = app . enableAutomaticUpdate ;
} ,
2019-09-17 16:16:48 +02:00
toggleAutomaticUpdates : function ( ) {
2019-09-10 19:21:30 +02:00
$scope . updates . busy = true ;
2019-09-17 16:16:48 +02:00
Client . configureApp ( $scope . app . id , 'automatic_update' , { enable : ! $scope . updates . enableAutomaticUpdate } , function ( error ) {
2019-09-10 19:21:30 +02:00
if ( error ) return Client . error ( error ) ;
2019-09-17 16:16:48 +02:00
$timeout ( function ( ) {
$scope . updates . enableAutomaticUpdate = ! $scope . updates . enableAutomaticUpdate ;
$scope . updates . busy = false ;
} , 1000 ) ;
} ) ;
} ,
check : function ( ) {
$scope . updates . busyCheck = true ;
Client . checkForUpdates ( function ( error ) {
if ( error ) Client . error ( error ) ;
$scope . updates . busyCheck = false ;
2019-09-10 19:21:30 +02:00
} ) ;
2019-09-17 17:14:40 +02:00
} ,
askUpdate : function ( ) {
$scope . updates . busyUpdate = false ;
$ ( '#updateModal' ) . modal ( 'show' ) ;
} ,
confirmUpdate : function ( ) {
$scope . updates . busyUpdate = true ;
2019-09-26 20:10:25 -07:00
Client . updateApp ( $scope . app . id , $scope . config . update . apps [ $scope . app . id ] . manifest , { skipBackup : $scope . updates . skipBackup } , function ( error ) {
2019-09-17 17:14:40 +02:00
$scope . updates . busyUpdate = false ;
if ( error ) return Client . error ( error ) ;
$ ( '#updateModal' ) . modal ( 'hide' ) ;
2019-12-16 16:08:49 -08:00
refreshApp ( $scope . app . id ) ;
2019-09-17 17:14:40 +02:00
} ) ;
2019-09-10 19:21:30 +02:00
}
} ;
$scope . backups = {
busy : false ,
2019-09-20 00:03:52 +02:00
busyCreate : false ,
2019-09-10 19:21:30 +02:00
error : { } ,
2019-09-13 17:07:45 +02:00
copyBackupIdDone : false ,
2019-09-10 19:21:30 +02:00
enableBackup : false ,
2019-09-13 17:07:45 +02:00
backups : [ ] ,
copyBackupId : function ( backup ) {
var copyText = document . getElementById ( 'backupIdHelper' ) ;
copyText . value = backup . id ;
copyText . select ( ) ;
document . execCommand ( 'copy' ) ;
$scope . backups . copyBackupIdDone = true ;
// reset after 2.5sec
$timeout ( function ( ) { $scope . backups . copyBackupIdDone = false ; } , 2500 ) ;
} ,
2019-09-20 00:03:52 +02:00
createBackup : function ( ) {
$scope . backups . busyCreate = true ;
Client . backupApp ( $scope . app . id , function ( error ) {
if ( error ) Client . error ( error ) ;
2019-12-16 16:17:13 -08:00
refreshApp ( $scope . app . id , function ( ) {
$scope . backups . busyCreate = false ;
waitForAppTask ( function ( error ) {
if ( error ) return Client . error ( error ) ;
$scope . backups . show ( ) ; // refresh backup listing
} ) ;
} ) ;
2019-09-20 00:03:52 +02:00
} ) ;
} ,
2019-09-10 19:21:30 +02:00
show : function ( ) {
var app = $scope . app ;
2019-09-19 18:31:11 +02:00
$scope . backups . error = { } ;
2019-09-10 19:21:30 +02:00
$scope . backups . enableBackup = app . enableBackup ;
2019-09-13 17:07:45 +02:00
Client . getAppBackups ( app . id , function ( error , backups ) {
if ( error ) return Client . error ( error ) ;
$scope . backups . backups = backups ;
} ) ;
2019-09-10 19:21:30 +02:00
} ,
2019-09-17 16:16:48 +02:00
toggleAutomaticBackups : function ( ) {
2019-09-10 19:21:30 +02:00
$scope . backups . busy = true ;
$scope . backups . error = { } ;
2019-09-17 16:16:48 +02:00
Client . configureApp ( $scope . app . id , 'automatic_backup' , { enable : ! $scope . backups . enableBackup } , function ( error ) {
2019-09-10 19:21:30 +02:00
if ( error ) return Client . error ( error ) ;
2019-09-17 16:16:48 +02:00
$timeout ( function ( ) {
$scope . backups . enableBackup = ! $scope . backups . enableBackup ;
$scope . backups . busy = false ;
} , 1000 ) ;
2019-09-10 19:21:30 +02:00
} ) ;
}
} ;
2019-09-13 11:18:43 +02:00
2020-02-06 16:08:22 -08:00
$scope . s3like = function ( provider ) {
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces'
2020-03-05 11:24:42 -08:00
|| provider === 'scaleway-objectstorage' || provider === 'wasabi'
2020-04-29 12:54:19 -07:00
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' ;
2020-02-06 16:08:22 -08:00
} ;
$scope . importBackup = {
busy : false ,
error : { } ,
provider : '' ,
bucket : '' ,
prefix : '' ,
accessKeyId : '' ,
secretAccessKey : '' ,
gcsKey : { keyFileName : '' , content : '' } ,
region : '' ,
endpoint : '' ,
backupFolder : '' ,
acceptSelfSignedCerts : false ,
format : 'tgz' ,
backupId : '' ,
2020-05-12 10:54:15 -07:00
password : '' ,
2020-02-06 16:08:22 -08:00
2020-02-07 10:22:52 -08:00
clearForm : function ( ) {
$scope . importBackup . bucket = '' ;
$scope . importBackup . prefix = '' ;
$scope . importBackup . accessKeyId = '' ;
$scope . importBackup . secretAccessKey = '' ;
$scope . importBackup . gcsKey . keyFileName = '' ;
$scope . importBackup . gcsKey . content = '' ;
$scope . importBackup . endpoint = '' ;
$scope . importBackup . region = '' ;
$scope . importBackup . backupFolder = '' ;
$scope . importBackup . format = 'tgz' ;
$scope . importBackup . acceptSelfSignedCerts = false ;
2020-05-12 10:54:15 -07:00
$scope . importBackup . password = '' ;
2020-02-07 11:16:14 -08:00
$scope . importBackup . backupId = '' ;
2020-02-07 10:22:52 -08:00
} ,
2020-02-06 16:08:22 -08:00
submit : function ( ) {
2020-02-07 10:22:52 -08:00
$scope . importBackup . error = { } ;
2020-02-06 16:08:22 -08:00
$scope . importBackup . busy = true ;
var backupConfig = {
provider : $scope . importBackup . provider ,
} ;
2020-05-12 10:54:15 -07:00
if ( $scope . importBackup . password ) backupConfig . password = $scope . importBackup . password ;
2020-02-06 16:08:22 -08:00
var backupId = $scope . importBackup . backupId ;
// only set provider specific fields, this will clear them in the db
if ( $scope . s3like ( backupConfig . provider ) ) {
backupConfig . bucket = $scope . importBackup . bucket ;
backupConfig . prefix = $scope . importBackup . prefix ;
backupConfig . accessKeyId = $scope . importBackup . accessKeyId ;
backupConfig . secretAccessKey = $scope . importBackup . secretAccessKey ;
if ( $scope . importBackup . endpoint ) backupConfig . endpoint = $scope . importBackup . endpoint ;
if ( backupConfig . provider === 's3' ) {
if ( $scope . importBackup . region ) backupConfig . region = $scope . importBackup . region ;
delete backupConfig . endpoint ;
} else if ( backupConfig . provider === 'minio' || backupConfig . provider === 's3-v4-compat' ) {
backupConfig . region = 'us-east-1' ;
backupConfig . acceptSelfSignedCerts = $scope . importBackup . acceptSelfSignedCerts ;
} else if ( backupConfig . provider === 'exoscale-sos' ) {
backupConfig . region = 'us-east-1' ;
backupConfig . signatureVersion = 'v4' ;
} else if ( backupConfig . provider === 'wasabi' ) {
backupConfig . region = 'us-east-1' ;
backupConfig . signatureVersion = 'v4' ;
} else if ( backupConfig . provider === 'scaleway-objectstorage' ) {
backupConfig . region = $scope . scalewayRegions . find ( function ( x ) { return x . value === $scope . importBackup . endpoint ; } ) . region ;
backupConfig . signatureVersion = 'v4' ;
2020-03-05 11:24:42 -08:00
} else if ( backupConfig . provider === 'linode-objectstorage' ) {
backupConfig . region = $scope . linodeRegions . find ( function ( x ) { return x . value === $scope . importBackup . endpoint ; } ) . region ;
2020-04-29 12:54:19 -07:00
backupConfig . signatureVersion = 'v4' ;
} else if ( backupConfig . provider === 'ovh-objectstorage' ) {
backupConfig . region = $scope . ovhRegions . find ( function ( x ) { return x . value === $scope . importBackup . endpoint ; } ) . region ;
2020-03-05 11:24:42 -08:00
backupConfig . signatureVersion = 'v4' ;
2020-02-06 16:08:22 -08:00
} else if ( backupConfig . provider === 'digitalocean-spaces' ) {
backupConfig . region = 'us-east-1' ;
}
} else if ( backupConfig . provider === 'gcs' ) {
backupConfig . bucket = $scope . importBackup . bucket ;
backupConfig . prefix = $scope . importBackup . prefix ;
try {
var serviceAccountKey = JSON . parse ( $scope . importBackup . gcsKey . content ) ;
backupConfig . projectId = serviceAccountKey . project _id ;
backupConfig . credentials = {
client _email : serviceAccountKey . client _email ,
private _key : serviceAccountKey . private _key
} ;
if ( ! backupConfig . projectId || ! backupConfig . credentials || ! backupConfig . credentials . client _email || ! backupConfig . credentials . private _key ) {
throw 'fields_missing' ;
}
} catch ( e ) {
$scope . importBackup . error . generic = 'Cannot parse Google Service Account Key: ' + e . message ;
$scope . importBackup . error . gcsKeyInput = true ;
$scope . importBackup . busy = false ;
return ;
}
} else if ( backupConfig . provider === 'filesystem' ) {
var parts = backupId . split ( '/' ) ;
backupId = parts . pop ( ) || parts . pop ( ) ; // removes any trailing slash. this is basename()
backupConfig . backupFolder = parts . join ( '/' ) ; // this is dirname()
2020-03-06 02:18:01 -08:00
}
2020-02-06 16:08:22 -08:00
2020-03-06 02:18:01 -08:00
if ( $scope . importBackup . format === 'tgz' ) {
if ( backupId . substring ( backupId . length - '.tar.gz' . length , backupId . length ) === '.tar.gz' ) { // endsWith
backupId = backupId . replace ( /.tar.gz$/ , '' ) ;
} else if ( backupId . substring ( backupId . length - '.tar.gz.enc' . length , backupId . length ) === '.tar.gz.enc' ) { // endsWith
backupId = backupId . replace ( /.tar.gz.enc$/ , '' ) ;
2020-02-06 16:08:22 -08:00
}
}
Client . importBackup ( $scope . app . id , backupId , $scope . importBackup . format , backupConfig , function ( error ) {
if ( error ) {
2020-02-07 10:22:52 -08:00
$scope . importBackup . busy = false ;
if ( error . statusCode === 424 ) {
$scope . importBackup . error . generic = error . message ;
if ( error . message . indexOf ( 'AWS Access Key Id' ) !== - 1 ) {
$scope . importBackup . error . accessKeyId = true ;
$scope . importBackupForm . accessKeyId . $setPristine ( ) ;
$ ( '#inputImportBackupAccessKeyId' ) . focus ( ) ;
2020-02-07 11:16:14 -08:00
} else if ( error . message . indexOf ( 'not match the signature' ) !== - 1 || error . message . indexOf ( 'Signature' ) !== - 1 ) {
2020-02-07 10:22:52 -08:00
$scope . importBackup . error . secretAccessKey = true ;
$scope . importBackupForm . secretAccessKey . $setPristine ( ) ;
$ ( '#inputImportBackupSecretAccessKey' ) . focus ( ) ;
} else if ( error . message . toLowerCase ( ) === 'access denied' ) {
2020-02-07 11:16:14 -08:00
$scope . importBackup . error . accessKeyId = true ;
$scope . importBackupForm . accessKeyId . $setPristine ( ) ;
2020-02-07 10:22:52 -08:00
$ ( '#inputImportBackupBucket' ) . focus ( ) ;
} else if ( error . message . indexOf ( 'ECONNREFUSED' ) !== - 1 ) {
$scope . importBackup . error . generic = 'Unknown region' ;
$scope . importBackup . error . region = true ;
$scope . importBackupForm . region . $setPristine ( ) ;
$ ( '#inputImportBackupDORegion' ) . focus ( ) ;
} else if ( error . message . toLowerCase ( ) === 'wrong region' ) {
$scope . importBackup . error . generic = 'Wrong S3 Region' ;
$scope . importBackup . error . region = true ;
$scope . importBackupForm . region . $setPristine ( ) ;
$ ( '#inputImportBackupS3Region' ) . focus ( ) ;
} else {
$scope . importBackup . error . bucket = true ;
$ ( '#inputImportBackupBucket' ) . focus ( ) ;
$scope . importBackupForm . bucket . $setPristine ( ) ;
}
} else if ( error . statusCode === 400 ) {
$scope . importBackup . error . generic = error . message ;
if ( $scope . importBackup . provider === 'filesystem' ) {
$scope . importBackup . error . backupFolder = true ;
}
} else {
Client . error ( error ) ;
}
2020-02-06 16:08:22 -08:00
return ;
}
$ ( '#importBackupModal' ) . modal ( 'hide' ) ;
2020-02-07 10:22:52 -08:00
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
$timeout ( function ( ) { $scope . importBackup . busy = false ; } , 1000 ) ;
} ) ;
2020-02-06 16:08:22 -08:00
} ) ;
} ,
show : function ( ) {
2020-02-07 10:22:52 -08:00
$scope . importBackup . clearForm ( ) ;
2020-02-06 16:08:22 -08:00
$ ( '#importBackupModal' ) . modal ( 'show' ) ;
}
} ;
2019-09-13 11:18:43 +02:00
$scope . uninstall = {
busy : false ,
error : { } ,
show : function ( ) {
2019-09-19 18:31:11 +02:00
$scope . uninstall . error = { } ;
2019-09-17 15:40:04 +02:00
} ,
ask : function ( ) {
2019-09-13 11:18:43 +02:00
$ ( '#uninstallModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
$scope . uninstall . busy = true ;
2019-09-29 16:52:57 -07:00
var NOOP = function ( next ) { return next ( ) ; } ;
var stopAppTask = $scope . app . taskId ? Client . stopTask . bind ( null , $scope . app . taskId ) : NOOP ;
stopAppTask ( function ( ) { // ignore error
Client . uninstallApp ( $scope . app . id , function ( error ) {
if ( error && error . statusCode === 402 ) { // unpurchase failed
Client . error ( 'Relogin to Cloudron App Store' ) ;
} else if ( error ) {
Client . error ( error ) ;
} else {
$ ( '#uninstallModal' ) . modal ( 'hide' ) ;
2019-10-11 14:55:19 -07:00
Client . refreshAppCache ( $scope . app . id , function ( ) { } ) ; // reflect the new app state immediately
2019-09-29 16:52:57 -07:00
$location . path ( '/apps' ) ;
}
2019-09-13 11:18:43 +02:00
2019-09-29 16:52:57 -07:00
$scope . uninstall . busy = false ;
} ) ;
2019-09-13 11:18:43 +02:00
} ) ;
}
} ;
2019-09-10 19:21:30 +02:00
2019-12-16 13:30:51 -08:00
$scope . console = {
2019-09-17 14:52:22 +02:00
show : function ( ) { } ,
2019-09-23 15:50:41 -07:00
busyRunState : false ,
2019-12-16 16:27:24 -08:00
startButton : false ,
2019-09-17 14:52:22 +02:00
2019-09-23 15:50:41 -07:00
toggleRunState : function ( ) {
var func = $scope . app . runState === RSTATES . STOPPED ? Client . startApp : Client . stopApp ;
2019-12-16 13:30:51 -08:00
$scope . console . busyRunState = true ;
2019-09-13 17:07:45 +02:00
2019-09-23 15:50:41 -07:00
func ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-09-22 12:21:39 +02:00
2019-12-20 19:09:17 -08:00
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-12-16 16:27:24 -08:00
2019-12-20 19:09:17 -08:00
$timeout ( function ( ) { $scope . console . busyRunState = false ; } , 1000 ) ;
} ) ;
2019-09-23 15:50:41 -07:00
} ) ;
2019-09-13 17:07:45 +02:00
}
} ;
2019-10-24 10:01:23 -07:00
$scope . restore = {
busy : false ,
error : { } ,
backup : null ,
show : function ( backup ) {
$scope . restore . error = { } ;
$scope . restore . backup = backup ;
$ ( '#restoreModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
$scope . restore . busy = true ;
Client . restoreApp ( $scope . app . id , $scope . restore . backup . id , function ( error ) {
if ( error ) {
Client . error ( error ) ;
$scope . restore . busy = false ;
return ;
}
$ ( '#restoreModal' ) . modal ( 'hide' ) ;
2019-12-16 16:08:49 -08:00
refreshApp ( $scope . app . id ) ;
2019-10-24 10:01:23 -07:00
} ) ;
}
} ;
2019-09-13 17:18:37 +02:00
$scope . clone = {
2019-09-24 18:50:52 +02:00
busy : false ,
2019-09-13 17:18:37 +02:00
error : { } ,
backup : null ,
location : '' ,
domain : null ,
portBindings : { } ,
portBindingsInfo : { } ,
portBindingsEnabled : { } ,
show : function ( backup ) {
var app = $scope . app ;
2019-09-19 18:31:11 +02:00
$scope . clone . error = { } ;
2019-09-13 17:18:37 +02:00
$scope . clone . backup = backup ;
$scope . clone . domain = $scope . domains . find ( function ( d ) { return app . domain === d . domain ; } ) ; // pre-select the app's domain
$scope . clone . portBindingsInfo = angular . extend ( { } , app . manifest . tcpPorts , app . manifest . udpPorts ) ; // Portbinding map only for information
// set default ports
for ( var env in $scope . clone . portBindingsInfo ) {
$scope . clone . portBindings [ env ] = $scope . clone . portBindingsInfo [ env ] . defaultValue || 0 ;
$scope . clone . portBindingsEnabled [ env ] = true ;
}
$ ( '#cloneModal' ) . modal ( 'show' ) ;
} ,
submit : function ( ) {
$scope . clone . busy = true ;
// only use enabled ports from portBindings
var finalPortBindings = { } ;
for ( var env in $scope . clone . portBindings ) {
if ( $scope . clone . portBindingsEnabled [ env ] ) {
finalPortBindings [ env ] = $scope . clone . portBindings [ env ] ;
}
}
var data = {
location : $scope . clone . location ,
domain : $scope . clone . domain . domain ,
portBindings : finalPortBindings ,
backupId : $scope . clone . backup . id
} ;
2019-09-24 18:50:52 +02:00
Client . checkDNSRecords ( data . domain , data . location , function ( error , result ) {
2019-09-13 17:18:37 +02:00
if ( error ) {
2019-09-24 18:50:52 +02:00
Client . error ( error ) ;
$scope . clone . busy = false ;
return ;
}
if ( result . error ) {
if ( result . error . reason === ERROR . ACCESS _DENIED ) {
$scope . clone . error . location = 'DNS credentials for ' + data . domain + ' are invalid. Update it in Domains & Certs view' ;
2019-09-13 17:18:37 +02:00
} else {
2019-09-24 18:50:52 +02:00
$scope . clone . error . location = result . error . message ;
2019-09-13 17:18:37 +02:00
}
2019-09-24 18:50:52 +02:00
$scope . clone . needsOverwrite = true ;
$scope . clone . busy = false ;
return ;
}
if ( result . needsOverwrite ) {
$scope . clone . error . location = 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron' ;
$scope . clone . needsOverwrite = true ;
$scope . clone . busy = false ;
2019-09-13 17:18:37 +02:00
return ;
}
2019-09-24 18:50:52 +02:00
Client . cloneApp ( $scope . app . id , data , function ( error /*, clonedApp */ ) {
$scope . clone . busy = false ;
if ( error ) {
if ( error . statusCode === 409 ) {
if ( error . portName ) {
$scope . clone . error . port = error . message ;
} else if ( error . domain ) {
$scope . clone . error . location = 'This location is already taken.' ;
$ ( '#cloneLocationInput' ) . focus ( ) ;
} else {
Client . error ( error ) ;
}
} else {
Client . error ( error ) ;
}
return ;
}
$ ( '#cloneModal' ) . modal ( 'hide' ) ;
2019-09-13 17:18:37 +02:00
2019-09-24 18:50:52 +02:00
$location . path ( '/apps' ) ;
} ) ;
2019-09-13 17:18:37 +02:00
} ) ;
}
2019-09-23 10:16:19 -07:00
} ;
2019-09-13 17:18:37 +02:00
2019-09-21 22:45:26 +02:00
$scope . repair = {
2019-12-20 11:18:48 -08:00
retryBusy : false ,
2019-09-21 22:45:26 +02:00
error : { } ,
2019-09-23 22:45:45 +02:00
location : null ,
domain : null ,
alternateDomains : [ ] ,
2019-09-21 22:45:26 +02:00
backups : [ ] ,
backupId : '' ,
2019-12-16 13:30:51 -08:00
show : function ( ) { } ,
2019-09-23 15:01:44 +02:00
// this prepares the repair dialog with whatever is required for repair action
2019-12-16 13:30:51 -08:00
confirm : function ( ) {
2019-09-21 22:45:26 +02:00
$scope . repair . error = { } ;
2019-12-20 11:18:48 -08:00
$scope . repair . retryBusy = false ;
2019-09-23 22:45:45 +02:00
$scope . repair . location = null ;
$scope . repair . domain = null ;
$scope . repair . alternateDomains = [ ] ;
2019-09-23 15:01:44 +02:00
$scope . repair . backupId = '' ;
2019-09-21 22:45:26 +02:00
2019-09-23 15:01:44 +02:00
var app = $scope . app ;
2019-09-21 22:45:26 +02:00
2019-12-06 10:13:30 -08:00
var errorState = ( $scope . app . error && $scope . app . error . installationState ) || ISTATES . PENDING _CONFIGURE ;
if ( errorState === ISTATES . PENDING _LOCATION _CHANGE ) {
2019-09-23 22:45:45 +02:00
$scope . repair . location = app . location ;
$scope . repair . domain = $scope . domains . filter ( function ( d ) { return d . domain === app . domain ; } ) [ 0 ] ;
$scope . repair . alternateDomains = $scope . app . alternateDomains ;
2019-09-27 15:34:54 -07:00
$scope . repair . alternateDomains = $scope . app . alternateDomains . map ( function ( altDomain ) {
return {
subdomain : altDomain . subdomain ,
enabled : true ,
domain : $scope . domains . filter ( function ( d ) { return d . domain === altDomain . domain ; } ) [ 0 ]
} ;
2019-09-23 15:01:44 +02:00
} ) ;
2019-09-23 22:45:45 +02:00
}
2019-09-21 22:45:26 +02:00
2019-12-06 10:13:30 -08:00
if ( errorState === ISTATES . PENDING _RESTORE ) {
2019-09-23 15:01:44 +02:00
Client . getAppBackups ( $scope . app . id , function ( error , backups ) {
if ( error ) return Client . error ( error ) ;
$scope . repair . backups = backups ;
$scope . repair . backupId = '' ;
2019-09-21 22:45:26 +02:00
2019-09-23 15:01:44 +02:00
$ ( '#repairModal' ) . modal ( 'show' ) ;
} ) ;
2019-09-23 22:45:45 +02:00
return ;
2019-09-23 15:01:44 +02:00
}
2019-09-23 22:45:45 +02:00
$ ( '#repairModal' ) . modal ( 'show' ) ;
2019-09-21 22:45:26 +02:00
} ,
submit : function ( ) {
$scope . repair . error = { } ;
2019-12-20 11:18:48 -08:00
$scope . repair . retryBusy = true ;
2019-09-21 22:45:26 +02:00
2019-12-06 10:13:30 -08:00
var errorState = ( $scope . app . error && $scope . app . error . installationState ) || ISTATES . PENDING _CONFIGURE ;
2019-09-23 22:45:45 +02:00
var data = { } ;
2019-12-06 10:13:30 -08:00
var repairFunc ;
switch ( errorState ) {
case ISTATES . PENDING _INSTALL :
case ISTATES . PENDING _CLONE : // if manifest or bad image, use CLI to provide new manifest
repairFunc = Client . repairApp . bind ( null , $scope . app . id , { } ) ; // this will trigger a re-install
break ;
case ISTATES . PENDING _LOCATION _CHANGE :
2019-09-23 22:45:45 +02:00
data . location = $scope . repair . location ;
data . domain = $scope . repair . domain . domain ;
2019-09-27 15:39:11 -07:00
data . alternateDomains = $scope . repair . alternateDomains . filter ( function ( a ) { return a . enabled ; } )
. map ( function ( d ) { return { subdomain : d . subdomain , domain : d . domain . domain } ; } ) ;
data . overwriteDns = true ; // always overwriteDns. user can anyway check and uncheck above
2019-12-06 10:13:30 -08:00
repairFunc = Client . configureApp . bind ( null , $scope . app . id , 'location' , data ) ;
break ;
case ISTATES . PENDING _DATA _DIR _MIGRATION :
repairFunc = Client . configureApp . bind ( null , $scope . app . id , 'data_dir' , { dataDir : null } ) ;
break ;
// this also happens for import faliures. this UI can only show backup listing. use CLI for arbit id/config
case ISTATES . PENDING _RESTORE :
repairFunc = Client . restoreApp . bind ( null , $scope . app . id , $scope . repair . backupId ) ;
break ;
case ISTATES . PENDING _UNINSTALL :
repairFunc = Client . uninstallApp . bind ( null , $scope . app . id ) ;
break ;
case ISTATES . PENDING _START :
case ISTATES . PENDING _STOP :
2019-12-20 11:18:48 -08:00
case ISTATES . PENDING _RESTART :
2019-12-06 10:13:30 -08:00
case ISTATES . PENDING _RESIZE :
case ISTATES . PENDING _DEBUG :
case ISTATES . PENDING _RECREATE _CONTAINER :
case ISTATES . PENDING _CONFIGURE :
2020-02-11 21:27:16 -08:00
case ISTATES . PENDING _BACKUP : // can happen if the backup task was killed/rebooted
2019-12-06 10:13:30 -08:00
case ISTATES . PENDING _UPDATE : // when update failed, just bring it back to current state and user can click update again
default :
repairFunc = Client . repairApp . bind ( null , $scope . app . id , { } ) ;
break ;
2019-09-23 22:45:45 +02:00
}
2019-12-06 10:13:30 -08:00
repairFunc ( function ( error ) {
2020-03-31 17:45:34 -07:00
$scope . repair . retryBusy = false ;
2019-09-21 22:45:26 +02:00
if ( error ) return Client . error ( error ) ;
2019-12-20 11:18:48 -08:00
$scope . repair . retryBusy = false ;
2019-09-21 22:45:26 +02:00
$ ( '#repairModal' ) . modal ( 'hide' ) ;
} ) ;
2019-12-16 13:30:51 -08:00
} ,
2019-12-20 11:18:48 -08:00
restartBusy : false ,
restartApp : function ( ) {
$scope . repair . restartBusy = true ;
Client . restartApp ( $scope . app . id , function ( error ) {
if ( error ) return console . error ( error ) ;
2019-12-20 19:09:17 -08:00
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-12-20 11:18:48 -08:00
2019-12-20 19:09:17 -08:00
$timeout ( function ( ) { $scope . repair . restartBusy = false ; } , 1000 ) ;
2019-12-20 11:18:48 -08:00
} ) ;
} ) ;
} ,
2019-12-16 18:18:22 -08:00
pauseBusy : false ,
pauseAppBegin : function ( ) {
$scope . repair . pauseBusy = true ;
Client . debugApp ( $scope . app . id , true , function ( error ) {
if ( error ) return console . error ( error ) ;
2019-12-20 19:09:17 -08:00
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-12-16 18:18:22 -08:00
2019-12-20 19:09:17 -08:00
$timeout ( function ( ) { $scope . repair . pauseBusy = false ; } , 1000 ) ;
2019-12-16 18:18:22 -08:00
} ) ;
} ) ;
} ,
pauseAppDone : function ( ) {
$scope . repair . pauseBusy = true ;
Client . debugApp ( $scope . app . id , false , function ( error ) {
if ( error ) return console . error ( error ) ;
2019-12-20 19:09:17 -08:00
refreshApp ( $scope . app . id , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-12-16 18:18:22 -08:00
2019-12-20 19:09:17 -08:00
$timeout ( function ( ) { $scope . repair . pauseBusy = false ; } , 1000 ) ;
2019-12-16 18:18:22 -08:00
} ) ;
} ) ;
}
2019-09-23 10:16:19 -07:00
} ;
2019-09-21 22:45:26 +02:00
2019-09-19 22:49:21 +02:00
$scope . postInstallConfirm = {
message : '' ,
confirmed : false ,
show : function ( ) {
$scope . postInstallConfirm . message = $scope . app . manifest . postInstallMessage ;
$scope . postInstallConfirm . confirmed = false ;
$ ( '#postInstallConfirmModal' ) . modal ( 'show' ) ;
return false ; // prevent propagation and default
} ,
submit : function ( ) {
if ( ! $scope . postInstallConfirm . confirmed ) return ;
$scope . app . pendingPostInstallConfirmation = false ;
delete localStorage [ 'confirmPostInstall_' + $scope . app . id ] ;
$ ( '#postInstallConfirmModal' ) . modal ( 'hide' ) ;
}
} ;
2019-09-10 19:21:30 +02:00
function fetchUsers ( callback ) {
Client . getUsers ( function ( error , users ) {
if ( error ) return callback ( error ) ;
// ensure we have something to work with in the access restriction dropdowns
users . forEach ( function ( user ) { user . display = user . username || user . email ; } ) ;
$scope . users = users ;
callback ( ) ;
} ) ;
}
function fetchGroups ( callback ) {
Client . getGroups ( function ( error , groups ) {
if ( error ) return callback ( error ) ;
$scope . groups = groups ;
callback ( ) ;
} ) ;
}
function getDomains ( callback ) {
Client . getDomains ( function ( error , result ) {
if ( error ) return callback ( error ) ;
$scope . domains = result ;
callback ( ) ;
} ) ;
}
function getBackupConfig ( callback ) {
Client . getBackupConfig ( function ( error , backupConfig ) {
if ( error ) return callback ( error ) ;
$scope . backupEnabled = backupConfig . provider !== 'noop' ;
callback ( ) ;
} ) ;
}
2019-12-16 16:08:49 -08:00
function refreshApp ( appId , callback ) {
2019-09-20 00:51:16 +02:00
callback = callback || function ( ) { } ;
2019-09-18 15:53:57 +02:00
2019-12-16 16:08:49 -08:00
Client . getApp ( appId , function ( error , app ) {
2019-10-01 20:04:28 +02:00
if ( error && error . statusCode === 404 ) return $location . path ( '/apps' ) ;
2019-09-12 16:28:21 +02:00
if ( error ) return callback ( error ) ;
2019-09-24 21:27:49 +02:00
// ensure we have amended progress properties set before copy
2019-12-16 16:22:29 -08:00
if ( $scope . app ) {
app . taskProgress = $scope . app . taskProgress ;
app . taskProgressMessage = $scope . app . taskProgressMessage ;
}
2019-09-24 21:27:49 +02:00
$scope . app = app ;
2019-12-16 16:27:24 -08:00
// show 'Start App' if app is starting or is stopped
if ( app . installationState === ISTATES . PENDING _START || app . installationState === ISTATES . PENDING _STOP ) {
$scope . console . startButton = app . installationState === ISTATES . PENDING _START ;
} else {
$scope . console . startButton = app . runState === RSTATES . STOPPED ;
}
2019-09-24 21:08:42 +02:00
if ( app . taskId ) {
Client . getTask ( app . taskId , function ( error , task ) {
if ( error ) return callback ( error ) ;
2019-09-24 21:27:49 +02:00
$scope . app . taskProgress = task && task . percent ? task . percent : 5 ; // start with 5 to avoid empty progress bar
$scope . app . taskProgressMessage = task ? task . message : '' ;
2020-04-02 12:19:42 +02:00
$scope . app . taskMinutesActive = task ? moment . duration ( moment . utc ( ) . diff ( moment . utc ( task . creationTime ) ) ) . asMinutes ( ) : 0 ;
2019-09-24 21:27:49 +02:00
callback ( ) ;
2019-09-24 21:08:42 +02:00
} ) ;
} else {
2019-09-24 21:27:49 +02:00
$scope . app . taskProgress = 0 ;
$scope . app . taskProgressMessage = '' ;
2020-03-26 00:16:23 +01:00
$scope . app . taskMinutesActive = 0 ;
2019-09-23 19:35:59 +02:00
2019-09-24 21:27:49 +02:00
callback ( ) ;
}
2019-09-12 16:28:21 +02:00
} ) ;
}
2019-09-24 21:08:42 +02:00
function waitForAppTask ( callback ) {
2019-09-20 00:51:16 +02:00
callback = callback || function ( ) { } ;
2019-09-17 16:16:48 +02:00
2019-09-20 00:03:52 +02:00
if ( ! $scope . app . taskId ) return callback ( ) ;
2019-09-24 21:08:42 +02:00
// app will be refreshed on interval
$timeout ( waitForAppTask . bind ( null , callback ) , 2000 ) ; // not yet done
2019-09-11 21:24:25 +02:00
}
2019-09-10 19:21:30 +02:00
Client . onReady ( function ( ) {
2019-12-16 16:22:29 -08:00
refreshApp ( appId , function ( error ) {
2019-09-10 19:21:30 +02:00
if ( error ) return Client . error ( error ) ;
2019-12-20 17:05:45 -08:00
// skipViewShow because we don't have all the values like domains/users to init the view yet
if ( $routeParams . view ) { // explicit route in url bar
$scope . setView ( $routeParams . view , true /* skipViewShow */ ) ;
} else { // default
$scope . setView ( $scope . app . error ? 'repair' : 'display' , true /* skipViewShow */ ) ;
}
2019-09-18 17:45:13 +02:00
2019-09-10 19:21:30 +02:00
asyncSeries ( [
fetchUsers ,
fetchGroups ,
getDomains ,
getBackupConfig
] , function ( error ) {
if ( error ) return Client . error ( error ) ;
2019-12-20 17:05:45 -08:00
$scope [ $scope . view ] . show ( ) ; // initialize now that we have all the values
var refreshTimer = $interval ( function ( ) { refreshApp ( $scope . app . id ) ; } , 2000 ) ; // call with inline function to avoid iteration argument passed see $interval docs
2019-09-18 18:18:43 +02:00
$scope . $on ( '$destroy' , function ( ) {
$interval . cancel ( refreshTimer ) ;
} ) ;
2019-09-10 19:21:30 +02:00
} ) ;
} ) ;
} ) ;
2019-09-12 16:28:21 +02:00
$ ( '#iconFileInput' ) . get ( 0 ) . onchange = function ( event ) {
var fr = new FileReader ( ) ;
fr . onload = function ( ) {
$scope . $apply ( function ( ) {
// var file = event.target.files[0];
$scope . display . icon . data = fr . result ;
} ) ;
} ;
fr . readAsDataURL ( event . target . files [ 0 ] ) ;
} ;
2019-09-10 19:21:30 +02:00
// setup all the dialog focus handling
2019-09-12 16:28:21 +02:00
[ 'appUninstallModal' , 'appUpdateModal' , 'appRestoreModal' ] . forEach ( function ( id ) {
2019-09-10 19:21:30 +02:00
$ ( '#' + id ) . on ( 'shown.bs.modal' , function ( ) {
2019-09-23 10:16:19 -07:00
$ ( this ) . find ( '[autofocus]:first' ) . focus ( ) ;
2019-09-10 19:21:30 +02:00
} ) ;
} ) ;
$ ( '.modal-backdrop' ) . remove ( ) ;
} ] ) ;