Files
cloudron-box/dashboard/public/views/app.js
T
Girish Ramakrishnan 41bc08a07e backup: move appConfig to backups table
this is useful for clone also to copy notes, operators, checklist
of the time when the backup was made (as opposed to current)

at this point, it's not clear why we need a archives table. it's
an optimization to not have to store icon for every backup.
2024-12-10 21:04:37 +01:00

2397 lines
95 KiB
JavaScript

'use strict';
/* global angular */
/* global $ */
/* global async */
/* global RSTATES */
/* global ISTATES */
/* global ERROR */
/* global Chart */
/* global Clipboard */
/* global SECRET_PLACEHOLDER */
/* global APP_TYPES, STORAGE_PROVIDERS, BACKUP_FORMATS */
/* global REGIONS_S3, REGIONS_WASABI, REGIONS_DIGITALOCEAN, REGIONS_EXOSCALE, REGIONS_SCALEWAY, REGIONS_LINODE, REGIONS_OVH, REGIONS_IONOS, REGIONS_UPCLOUD, REGIONS_VULTR, REGIONS_HETZNER */
/* global onAppClick */
angular.module('Application').controller('AppController', ['$scope', '$location', '$translate', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $translate, $timeout, $interval, $route, $routeParams, Client) {
$scope.s3Regions = REGIONS_S3;
$scope.wasabiRegions = REGIONS_WASABI;
$scope.doSpacesRegions = REGIONS_DIGITALOCEAN;
$scope.exoscaleSosRegions = REGIONS_EXOSCALE;
$scope.scalewayRegions = REGIONS_SCALEWAY;
$scope.linodeRegions = REGIONS_LINODE;
$scope.ovhRegions = REGIONS_OVH;
$scope.ionosRegions = REGIONS_IONOS;
$scope.upcloudRegions = REGIONS_UPCLOUD;
$scope.vultrRegions = REGIONS_VULTR;
$scope.contaboRegions = REGIONS_VULTR;
$scope.hetznerRegions = REGIONS_HETZNER;
$scope.storageProviders = STORAGE_PROVIDERS;
$scope.formats = BACKUP_FORMATS;
// 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');
$scope.view = '';
$scope.app = null;
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
// note: these variables will remain empty for operators
$scope.domains = [];
$scope.volumes = [];
$scope.groups = [];
$scope.users = [];
$scope.backupConfig = null;
$scope.diskUsage = -1;
$scope.diskUsageDate = 0;
$scope.APP_TYPES = APP_TYPES;
$scope.HOST_PORT_MIN = 1;
$scope.HOST_PORT_MAX = 65535;
$scope.ROBOTS_DISABLE_INDEXING_TEMPLATE = '# Disable search engine indexing\n\nUser-agent: *\nDisallow: /';
$scope.setView = function (view, skipViewShow) {
if ($scope.view === view) return;
$route.updateParams({ view: view });
if (!skipViewShow) $scope[view].show();
$scope.view = view;
};
$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);
});
};
$scope.appPostInstallConfirm = {
app: {},
message: '',
confirmed: false,
show: function (app) {
$scope.appPostInstallConfirm.app = app;
$scope.appPostInstallConfirm.message = app.manifest.postInstallMessage;
$scope.appPostInstallConfirm.confirmed = false;
$('#appPostInstallConfirmModal').modal('show');
return false; // prevent propagation and default
},
submit: function () {
if (!$scope.appPostInstallConfirm.confirmed) return;
$scope.appPostInstallConfirm.app.pendingPostInstallConfirmation = false;
delete localStorage['confirmPostInstall_' + $scope.appPostInstallConfirm.app.id];
$('#appPostInstallConfirmModal').modal('hide');
}
};
$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');
}
};
$scope.getAppBackupDownloadLink = function (backup) {
return Client.getAppBackupDownloadLink($scope.app.id, backup.id);
};
$scope.onAppClick = function (app, $event) { onAppClick(app, $event, true /* always operator */, $scope); };
$scope.sftpInfo = {
show: function () {
$('#sftpInfoModal').modal('show');
}
};
$scope.info = {
showDoneChecklist: false,
hasOldChecklist: false,
notes: {
busy: true,
busySave: false,
editing: false,
content: '',
placeholder: 'Add admin notes here...',
edit: function () {
$scope.info.notes.content = $scope.app.notes === null ? $scope.app.manifest.postInstallMessage : $scope.app.notes;
$scope.info.notes.editing = true;
setTimeout(function () { document.getElementById('adminNotesTextarea').focus(); }, 1);
},
dismiss: function () {
$scope.info.notes.content = $scope.app.notes === null ? $scope.app.manifest.postInstallMessage : $scope.app.notes;
$scope.info.notes.editing = false;
},
submit: function () {
$scope.info.notes.busySave = true;
// skip saving if unchanged from postInstall
if ($scope.info.notes.content === $scope.app.manifest.postInstallMessage) {
$scope.info.notes.busySave = false;
$scope.info.notes.editing = false;
return;
}
Client.configureApp($scope.app.id, 'notes', { notes: $scope.info.notes.content }, function (error) {
if (error) return console.error('Failed to save notes.', error);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$scope.info.notes.content = $scope.app.notes === null ? $scope.app.manifest.postInstallMessage : $scope.app.notes;
$scope.info.notes.busySave = false;
$scope.info.notes.editing = false;
});
});
}
},
show: function () {
$scope.info.hasOldChecklist = !!Object.keys($scope.app.checklist).find((k) => { return $scope.app.checklist[k].acknowledged; });
$scope.info.notes.content = $scope.app.notes === null ? $scope.app.manifest.postInstallMessage : $scope.app.notes;
$scope.info.notes.editing = false;
$scope.info.notes.busy = false;
},
checklistAck(item, key) {
item.acknowledged = true;
// item.acknowledged = !item.acknowledged;
Client.ackAppChecklistItem($scope.app.id, key, item.acknowledged, function (error) {
if (error) return console.error('Failed to ack checklist item.', error);
$scope.info.hasOldChecklist = true;
refreshApp($scope.app.id);
});
}
};
$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;
$scope.display.error = {};
// translate for tag-input
$scope.display.tags = app.tags ? app.tags.join(' ') : '';
$scope.display.label = $scope.app.label || '';
$scope.display.icon = { data: null };
},
submit: function () {
$scope.display.busy = true;
$scope.display.error = {};
function done(error) {
if (error) Client.error(error);
$scope.displayForm.$setPristine();
$scope.display.success = true;
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);
});
}
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) {
if (error) return done(error);
var tags = $scope.display.tags.split(' ').map(function (t) { return t.trim(); }).filter(function (t) { return !!t; });
var configureTags = angular.equals(tags, $scope.app.tags) ? NOOP : Client.configureApp.bind(null, $scope.app.id, 'tags', { tags: tags });
configureTags(function (error) {
if (error) return done(error);
// skip if icon is unchanged
if ($scope.display.icon.data === null) return done();
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) {
if (error) return done(error);
done();
});
});
});
}
};
$scope.location = {
busy: false,
error: {},
domainCollisions: [],
domain: null, // object and not the string
subdomain: '',
secondaryDomains: {},
redirectDomains: [],
aliasDomains: [],
ports: {},
portsEnabled: {},
portInfo: {},
addRedirectDomain: function (event) {
event.preventDefault();
$scope.location.redirectDomains.push({
domain: $scope.domains.filter(function (d) { return d.domain === $scope.app.domain; })[0], // pre-select app's domain by default
subdomain: ''
});
setTimeout(function () {
document.getElementById('redirectDomainsInput-' + ($scope.location.redirectDomains.length-1)).focus();
}, 200);
},
delRedirectDomain: function (event, index) {
event.preventDefault();
$scope.location.redirectDomains.splice(index, 1);
},
addAliasDomain: function (event) {
event.preventDefault();
$scope.location.aliasDomains.push({
domain: $scope.domains.filter(function (d) { return d.domain === $scope.app.domain; })[0], // pre-select app's domain by default
subdomain: ''
});
setTimeout(function () {
document.getElementById('aliasDomainsInput-' + ($scope.location.aliasDomains.length-1)).focus();
}, 200);
},
delAliasDomain: function (event, index) {
event.preventDefault();
$scope.location.aliasDomains.splice(index, 1);
},
show: function () {
var app = $scope.app;
$scope.location.error = {};
$scope.location.domainCollisions = [];
$scope.location.subdomain = app.subdomain;
$scope.location.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
// for compat, secondary domain can be empty after an upgrade. so it may not exist in app.secondaryDomains
$scope.location.secondaryDomains = {};
var httpPorts = app.manifest.httpPorts || {};
for (var env2 in httpPorts) {
$scope.location.secondaryDomains[env2] = {
subdomain: httpPorts[env2].defaultValue || '',
domain: $scope.location.domain
};
}
// now fill secondaryDomains with real values, if it exists
app.secondaryDomains.forEach(function (sd) {
$scope.location.secondaryDomains[sd.environmentVariable] = {
subdomain: sd.subdomain,
domain: $scope.domains.filter(function (d) { return d.domain === sd.domain; })[0]
};
});
$scope.location.portInfo = angular.extend({}, app.manifest.tcpPorts, app.manifest.udpPorts); // Portbinding map only for information
$scope.location.redirectDomains = app.redirectDomains.map(function (a) { return { subdomain: a.subdomain, domain: $scope.domains.filter(function (d) { return d.domain === a.domain; })[0] };});
$scope.location.aliasDomains = app.aliasDomains.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.portInfo) {
if (app.portBindings && app.portBindings[env]) {
$scope.location.ports[env] = app.portBindings[env].hostPort;
$scope.location.portsEnabled[env] = true;
} else {
$scope.location.ports[env] = $scope.location.portInfo[env].defaultValue || 0;
$scope.location.portsEnabled[env] = false;
}
}
},
submit: function (overwriteDns) {
$('#domainCollisionsModal').modal('hide');
$scope.location.busy = true;
$scope.location.error = {};
$scope.location.domainCollisions = [];
var secondaryDomains = {};
for (var env2 in $scope.location.secondaryDomains) {
secondaryDomains[env2] = {
subdomain: $scope.location.secondaryDomains[env2].subdomain,
domain: $scope.location.secondaryDomains[env2].domain.domain
};
}
// only use enabled ports
var ports = {};
for (var env in $scope.location.ports) {
if ($scope.location.portsEnabled[env]) {
ports[env] = $scope.location.ports[env];
}
}
var data = {
overwriteDns: !!overwriteDns,
subdomain: $scope.location.subdomain,
domain: $scope.location.domain.domain,
ports: ports,
secondaryDomains: secondaryDomains,
redirectDomains: $scope.location.redirectDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };}),
aliasDomains: $scope.location.aliasDomains.map(function (a) { return { subdomain: a.subdomain, domain: a.domain.domain };})
};
// pre-flight only for changed domains
var domains = [];
if ($scope.app.domain !== data.domain || $scope.app.subdomain !== data.subdomain) domains.push({ subdomain: data.subdomain, domain: data.domain, type: 'primary' });
Object.keys(data.secondaryDomains).forEach(function (env) {
var subdomain = data.secondaryDomains[env].subdomain, domain = data.secondaryDomains[env].domain;
if ($scope.app.secondaryDomains.some(function (d) { return d.domain === domain && d.subdomain === subdomain; })) return;
domains.push({ subdomain: subdomain, domain: domain, type: 'secondary' });
});
data.redirectDomains.forEach(function (a) {
if ($scope.app.redirectDomains.some(function (d) { return d.domain === a.domain && d.subdomain === a.subdomain; })) return;
domains.push({ subdomain: a.subdomain, domain: a.domain, type: 'redirect' });
});
data.aliasDomains.forEach(function (a) {
if ($scope.app.aliasDomains.some(function (d) { return d.domain === a.domain && d.subdomain === a.subdomain; })) return;
domains.push({ subdomain: a.subdomain, domain: a.domain, type: 'alias' });
});
var canConfigure = true;
async.eachSeries(domains, function (domain, callback) {
if (overwriteDns) return callback();
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
if (error) return callback(error);
if (result.error) {
if (domain.type === 'primary') {
$scope.location.error.location = domain.domain + ' ' + result.error.message;
} else if (domain.type === 'alias') {
$scope.location.error.aliasDomains = domain.domain + ' ' + result.error.message;
} else {
$scope.location.error.redirectDomains = domain.domain + ' ' + result.error.message;
}
$scope.location.busy = false;
canConfigure = false;
} else if (result.needsOverwrite) {
$scope.location.domainCollisions.push(domain);
canConfigure = false;
}
callback();
});
}, function (error) {
if (error) {
$scope.location.busy = false;
return Client.error(error);
}
if (!canConfigure) {
$scope.location.busy = false;
return $('#domainCollisionsModal').modal('show');
}
Client.configureApp($scope.app.id, 'location', data, function (error) {
if (error && (error.statusCode === 409 || error.statusCode === 400)) {
var errorMessage = error.message.toLowerCase();
if (errorMessage.indexOf('location') !== -1) {
if (errorMessage.indexOf('primary') !== -1) {
$scope.location.error.location = error.message;
$scope.locationForm.$setPristine();
} else if (errorMessage.indexOf('secondary') !== -1) {
$scope.location.error.secondaryDomain = error.message;
} else if (errorMessage.indexOf('redirect') !== -1) {
$scope.location.error.redirectDomains = error.message;
} else if (errorMessage.indexOf('alias') !== -1) {
$scope.location.error.aliasDomains = error.message;
}
} else if (errorMessage.indexOf('port') !== -1) {
$scope.location.error.port = error.message;
} else {
$scope.location.error.location = error.message; // fallback
}
$scope.location.busy = false;
return;
}
if (error) return Client.error(error);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$scope.locationForm.$setPristine();
$timeout(function () { $scope.location.busy = false; }, 1000);
});
});
});
}
};
$scope.access = {
busy: false,
error: {},
success: false,
ftp: false,
ssoAuth: false,
accessRestrictionOption: 'any',
accessRestrictionOptionCur: 'any',
accessRestriction: { users: [], groups: [] },
operators: { users: [], groups: [] },
isAccessRestrictionValid: function () {
var tmp = $scope.access.accessRestriction;
return !!(tmp.users.length || tmp.groups.length);
},
show: function () {
var app = $scope.app;
$scope.access.error = {};
$scope.access.ftp = app.manifest.addons.localstorage && app.manifest.addons.localstorage.ftp;
$scope.access.ssoAuth = (app.manifest.addons['ldap'] || app.manifest.addons['oidc'] || app.manifest.addons['proxyAuth']) && app.sso;
$scope.access.accessRestrictionOption = app.accessRestriction ? 'groups' : 'any';
$scope.access.accessRestrictionOptionCur = app.accessRestriction ? 'groups' : 'any';
$scope.access.accessRestriction = { users: [], groups: [] };
$scope.access.operators = { users: [], groups: [] };
var userSet, groupSet;
if (app.accessRestriction) {
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); });
groupSet = {};
if (app.accessRestriction.groups) 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); });
}
if (app.operators) {
userSet = {};
app.operators.users.forEach(function (uid) { userSet[uid] = true; });
$scope.users.forEach(function (u) { if (userSet[u.id] === true) $scope.access.operators.users.push(u); });
groupSet = {};
if (app.operators.groups) app.operators.groups.forEach(function (gid) { groupSet[gid] = true; });
$scope.groups.forEach(function (g) { if (groupSet[g.id] === true) $scope.access.operators.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; });
}
var operators = null;
if ($scope.access.operators.users.length || $scope.access.operators.groups.length) {
operators = { users: [], groups: [] };
operators.users = $scope.access.operators.users.map(function (u) { return u.id; });
operators.groups = $scope.access.operators.groups.map(function (g) { return g.id; });
}
async.series([
function (callback) {
if ($scope.access.accessRestrictionOption === $scope.access.accessRestrictionOptionCur && !$scope.accessForm.accessUsersSelect.$dirty && !$scope.accessForm.accessGroupsSelect.$dirty) return callback();
Client.configureApp($scope.app.id, 'access_restriction', { accessRestriction: accessRestriction }, callback);
},
function (callback) {
if (!$scope.accessForm.operatorsUsersSelect.$dirty && !$scope.accessForm.operatorsGroupsSelect.$dirty) return callback();
Client.configureApp($scope.app.id, 'operators', { operators: operators }, callback);
}
], function (error) {
if (error) return Client.error(error);
$scope.accessForm.$setPristine();
$scope.access.accessRestrictionOptionCur = $scope.access.accessRestrictionOption;
$timeout(function () {
$scope.access.success = true;
$scope.access.busy = false;
}, 3000);
});
}
};
$scope.resources = {
error: {},
busy: false,
currentMemoryLimit: 0,
memoryLimit: 0, // RAM
memoryTicks: [],
currentCpuQuota: 0,
cpuQuota: 0,
devices: '',
show: function () {
var app = $scope.app;
$scope.resources.busy = true;
$scope.resources.error = {};
$scope.resources.currentMemoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
Client.memory(function (error, result) {
if (error) console.error(error);
// create ticks starting from manifest memory limit. the memory limit here is just RAM
$scope.resources.memoryTicks = [];
// we max system memory and current app memory for the case where the user configured the app on another server with more resources
var nearest256m = Math.ceil(Math.max(result.memory, $scope.resources.currentMemoryLimit) / (256*1024*1024)) * 256 * 1024 * 1024;
var startTick = app.manifest.memoryLimit || (256 * 1024 * 1024);
// code below ensure we atleast have 2 ticks to keep the slider usable
$scope.resources.memoryTicks.push(startTick); // start tick
for (var i = startTick * 2; i < nearest256m; i *= 2) {
$scope.resources.memoryTicks.push(i);
}
$scope.resources.memoryTicks.push(nearest256m); // end tick
});
// for firefox widget update
$timeout(function() {
$scope.resources.currentCpuQuota = $scope.resources.cpuQuota = app.cpuQuota;
$scope.resources.memoryLimit = $scope.resources.currentMemoryLimit;
$scope.resources.busy = false;
}, 500);
$scope.resources.devices = Object.keys(app.devices).join(', ');
},
submitMemoryLimit: function () {
$scope.resources.busy = true;
$scope.resources.error = {};
const tmp = parseInt($scope.resources.memoryLimit);
const memoryLimit = tmp === $scope.resources.memoryTicks[0] ? 0 : tmp;
Client.configureApp($scope.app.id, 'memory_limit', { memoryLimit }, function (error) {
if (error && error.statusCode === 400) {
$scope.resources.busy = false;
$scope.resources.error.memoryLimit = true;
return;
}
if (error) return Client.error(error);
$scope.resources.currentMemoryLimit = $scope.resources.memoryLimit;
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.resources.busy = false; }, 1000);
});
});
},
submitCpuQuota: function () {
$scope.resources.busy = true;
$scope.resources.error = {};
Client.configureApp($scope.app.id, 'cpu_quota', { cpuQuota: parseInt($scope.resources.cpuQuota) }, function (error) {
if (error) return Client.error(error);
$scope.resources.currentCpuQuota = $scope.resources.cpuQuota;
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.resources.busy = false; }, 1000);
});
});
},
submitDevices: function () {
$scope.resources.busy = true;
$scope.resources.error = {};
const devices = {};
$scope.resources.devices.split(',').forEach(d => {
if (!d.trim()) return;
devices[d.trim()] = {};
});
Client.configureApp($scope.app.id, 'devices', { devices }, function (error) {
if (error && error.statusCode === 400) {
$scope.resources.error.devices = error.message;
return $scope.resources.busy = false;
} else if (error) {
return Client.error(error);
}
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.resources.busy = false; }, 1000);
});
});
},
};
$scope.services = {
error: {},
busy: false,
enableTurn: '1', // curse of radio buttons
enableRedis: '1',
show: function () {
var app = $scope.app;
$scope.services.error = {};
$scope.services.enableTurn = app.enableTurn ? '1' : '0';
$scope.services.enableRedis = app.enableRedis ? '1' : '0';
},
submitTurn: function () {
$scope.services.busy = true;
$scope.services.error = {};
Client.configureApp($scope.app.id, 'turn', { enable: $scope.services.enableTurn === '1' }, function (error) {
if (error && error.statusCode === 400) {
$scope.services.busy = false;
$scope.services.error.turn = true;
return;
}
if (error) return Client.error(error);
$timeout(function () { $scope.services.busy = false; }, 1000);
});
},
submitRedis: function () {
$scope.services.busy = true;
$scope.services.error = {};
Client.configureApp($scope.app.id, 'redis', { enable: $scope.services.enableRedis === '1' }, function (error) {
if (error && error.statusCode === 400) {
$scope.services.busy = false;
$scope.services.error.redis = true;
return;
}
if (error) return Client.error(error);
$timeout(function () { $scope.services.busy = false; }, 1000);
});
},
};
$scope.storage = {
error: {},
busy: false,
busyDataDir: false,
storageVolumeId: null,
storageVolumePrefix: '',
location: null,
locationOptions: [],
busyBinds: false,
mounts: [], // { volume, readOnly }
show: function () {
var app = $scope.app;
$scope.storage.error = {};
$scope.storage.storageVolumeId = app.storageVolumeId;
$scope.storage.storageVolumePrefix = app.storageVolumePrefix || '';
$scope.storage.mounts = [];
$scope.storage.locationOptions = [
{ id: 'default', type: 'default', displayName: 'Default - /home/yellowtent/appsdata/' + app.id },
];
$scope.volumes.forEach(function (volume) {
$scope.storage.locationOptions.push({ id: volume.id, type: 'volume', value: volume.id, displayName: 'Volume - ' + volume.name, mountType: volume.mountType });
});
$scope.storage.location = $scope.storage.locationOptions.find(function (l) { return l.id === (app.storageVolumeId || 'default'); });
app.mounts.forEach(function (mount) { // { volumeId, readOnly }
var volume = $scope.volumes.find(function (v) { return v.id === mount.volumeId; });
$scope.storage.mounts.push({ volume: volume, readOnly: mount.readOnly ? 'true' : 'false' });
});
},
submitDataDir: function () {
$scope.storage.busyDataDir = true;
$scope.storage.error = {};
var data = { storageVolumeId: null, storageVolumePrefix: null };
if ($scope.storage.location.id !== 'default') {
data.storageVolumeId = $scope.storage.location.id;
data.storageVolumePrefix = $scope.storage.storageVolumePrefix;
}
Client.configureApp($scope.app.id, 'storage', data, function (error) {
if (error && error.statusCode === 400) {
$scope.storage.error.storageVolumePrefix = error.message;
$scope.storage.busyDataDir = false;
return;
} else if (error) {
Client.error(error);
$scope.storage.busyDataDir = false;
return;
}
$scope.storageDataDirForm.$setPristine();
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.storage.busyDataDir = false; }, 1000);
});
});
},
addMount: function (event) {
event.preventDefault();
$scope.storage.mounts.push({
volume: $scope.volumes[0],
readOnly: true
});
},
delMount: function (event, index) {
event.preventDefault();
$scope.storage.mounts.splice(index, 1);
},
submitMounts: function () {
$scope.storage.busyMounts = true;
$scope.storage.error = {};
var data = [];
$scope.storage.mounts.forEach(function (mount) {
data.push({ volumeId: mount.volume.id, readOnly: mount.readOnly === 'true' });
});
Client.configureApp($scope.app.id, 'mounts', { mounts: data }, function (error) {
if (error && error.statusCode === 400) {
$scope.storage.error.mounts = error.message;
$scope.storage.busyMounts = false;
return;
}
if (error) return Client.error(error);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.storage.busyMounts = false; }, 1000);
});
});
}
};
$scope.graphs = {
error: {},
busy: true,
period: 6,
memoryChart: null,
diskChart: null,
blockReadTotal: 0,
blockWriteTotal: 0,
networkReadTotal: 0,
networkWriteTotal: 0,
setPeriod: function (hours) {
$scope.graphs.period = hours;
$scope.graphs.show();
},
show: function () {
$scope.graphs.busy = true;
// in minutes
var timePeriod = $scope.graphs.period * 60;
// keep in sync with graphs.js
var timeBucketSizeMinutes = timePeriod > (24 * 60) ? (6*60) : 5;
var steps = Math.floor(timePeriod/timeBucketSizeMinutes);
var labels = new Array(steps).fill(0);
labels = labels.map(function (v, index) {
var dateTime = new Date(Date.now() - ((timePeriod - (index * timeBucketSizeMinutes)) * 60 * 1000));
if ($scope.graphs.period > 24) {
return dateTime.toLocaleDateString();
} else {
return dateTime.toLocaleTimeString();
}
});
var borderColors = [ '#2196F3', '#FF6384' ];
var backgroundColors = [ '#82C4F844', '#FF63844F' ];
function fillGraph(canvasId, contents, chartPropertyName, divisor, max, format, formatDivisor, stepSize) {
if (!contents || !contents[0]) return; // no data available yet
var datasets = [];
contents.forEach(function (content, index) {
// fill holes with previous value
var cur = 0;
content.data.forEach(function (d) {
if (d[0] === null) d[0] = cur;
else cur = d[0];
});
var datapoints = Array(steps).map(function () { return '0'; });
// walk backwards and fill up the datapoints
content.data.reverse().forEach(function (d, index) {
datapoints[datapoints.length-1-index] = (d[0] / divisor).toFixed(2);
// return parseInt((d[0] / divisor).toFixed(2));
});
datasets.push({
label: content.label,
backgroundColor: backgroundColors[index],
borderColor: borderColors[index],
borderWidth: 1,
radius: 0,
data: datapoints,
cubicInterpolationMode: 'monotone',
tension: 0.4
});
});
var graphData = {
labels: labels,
datasets: datasets
};
var options = {
responsive: true,
maintainAspectRatio: true,
aspectRatio: 2.5,
animation: false,
plugins: {
legend: {
display: false
}
},
interaction: {
intersect: false,
mode: 'index',
},
scales: {
x: {
ticks: { autoSkipPadding: 50, maxRotation: 0 }
},
y: {
ticks: { maxTicksLimit: 6 },
min: 0,
beginAtZero: true
}
}
};
if (format) options.scales.y.ticks.callback = function (value) {
if (!formatDivisor) return value + ' ' + format;
return (value/formatDivisor).toLocaleString('en-US', { maximumFractionDigits: 6 }) + ' ' + format;
};
if (max) options.scales.y.max = max;
if (stepSize) options.scales.y.ticks.stepSize = stepSize;
var ctx = $(canvasId).get(0).getContext('2d');
if ($scope.graphs[chartPropertyName]) $scope.graphs[chartPropertyName].destroy();
$scope.graphs[chartPropertyName] = new Chart(ctx, { type: 'line', data: graphData, options: options });
}
Client.getAppGraphs(appId, timePeriod, function (error, result) {
if (error) return console.error(error);
var currentMemoryLimit = $scope.app.memoryLimit || $scope.app.manifest.memoryLimit || 0;
var maxGraphMemory = currentMemoryLimit < (512 * 1024 * 1024) ? (512 * 1024 * 1024) : currentMemoryLimit;
var cpuCount = result.cpuCount;
var ioDivisor = 1000 * 1000;
$scope.graphs.blockReadTotal = (result.blockReadTotal / ioDivisor / 1000).toFixed(2) + ' MB';
$scope.graphs.blockWriteTotal = (result.blockWriteTotal / ioDivisor / 1000).toFixed(2) + ' MB';
$scope.graphs.networkReadTotal = (result.networkReadTotal / ioDivisor / 1000).toFixed(2) + ' MB';
$scope.graphs.networkWriteTotal = (result.networkWriteTotal / ioDivisor / 1000).toFixed(2) + ' MB';
fillGraph('#graphsMemoryChart', [{ data: result.memory, label: 'Memory' }], 'memoryChart', 1024 * 1024, maxGraphMemory / 1024 / 1024, 'GiB', 1024, (maxGraphMemory / 1024 / 1024) <= 1024 ? 256 : 512);
fillGraph('#graphsCpuChart', [{ data: result.cpu, label: 'CPU' }], 'cpuChart', 1, cpuCount * 100, '%');
fillGraph('#graphsDiskChart', [{ data: result.blockRead, label: 'read' }, { data: result.blockWrite, label: 'write' }], 'diskChart', ioDivisor, null, 'kB/s');
fillGraph('#graphsNetworkChart', [{ data: result.networkRead, label: 'inbound' }, { data: result.networkWrite, label: 'outbound' }], 'networkChart', ioDivisor, null, 'kB/s');
$scope.graphs.busy = false;
});
}
};
function findInbox(inboxes, app) {
return inboxes.find(function (i) { return i.name === app.inboxName && i.domain === (app.inboxDomain || app.domain); });
}
$scope.email = {
enableMailbox: true,
mailboxName: '',
mailboxDomain: null,
mailboxDisplayName: '',
currentMailboxName: '',
currentMailboxDomainName: '',
mailboxError: {},
mailboxBusy: false,
inboxError: {},
inboxBusy: false,
enableInbox: true,
inboxes: [],
currentInbox: null,
inbox: null,
show: function () {
var app = $scope.app;
$scope.emailForm.$setPristine();
$scope.email.mailboxError = {};
$scope.email.enableMailbox = app.enableMailbox ? '1' : '0';
$scope.email.mailboxName = app.mailboxName || '';
$scope.email.mailboxDisplayName = app.mailboxDisplayName || '';
$scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === (app.mailboxDomain || app.domain); })[0];
$scope.email.currentMailboxName = app.mailboxName || '';
$scope.email.currentMailboxDomainName = $scope.email.mailboxDomain ? $scope.email.mailboxDomain.domain : '';
$scope.email.inboxError = {};
$scope.email.enableInbox = app.enableInbox ? true : false;
Client.getAllMailboxes(function (error, mailboxes) {
if (error) console.error('Failed to list mailboxes.', error);
$scope.email.inboxes = mailboxes.map(function (m) { return { display: m.name + '@' + m.domain, name: m.name, domain: m.domain }; });
$scope.email.currentInbox = findInbox($scope.email.inboxes, app);
$scope.email.inbox = findInbox($scope.email.inboxes, app);
});
},
submitMailbox: function () {
$scope.email.error = {};
$scope.email.mailboxBusy = true;
var data = {
enable: $scope.email.enableMailbox === '1'
};
if (data.enable) {
data.mailboxName = $scope.email.mailboxName || null;
data.mailboxDomain = $scope.email.mailboxDomain.domain;
data.mailboxDisplayName = $scope.email.mailboxDisplayName;
}
Client.configureApp($scope.app.id, 'mailbox', data, function (error) {
if (error && error.statusCode === 400) {
$scope.email.mailboxBusy = false;
$scope.email.error.mailboxName = error.message;
$scope.emailForm.$setPristine();
return;
}
if (error) return Client.error(error);
$scope.emailForm.$setPristine();
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
// when the mailboxName is 'reset', this will fill it up with the default again
$scope.email.enableMailbox = $scope.app.enableMailbox ? '1' : '0';
$scope.email.mailboxName = $scope.app.mailboxName || '';
$scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === ($scope.app.mailboxDomain || $scope.app.domain); })[0];
$scope.email.currentMailboxName = $scope.app.mailboxName || '';
$scope.email.currentMailboxDomainName = $scope.email.mailboxDomain ? $scope.email.mailboxDomain.domain : '';
$timeout(function () { $scope.email.mailboxBusy = false; }, 1000);
});
});
},
submitInbox: function () {
$scope.email.error = {};
$scope.email.inboxBusy = true;
var data = {
enable: $scope.email.enableInbox
};
if (data.enable) {
data.inboxName = $scope.email.inbox.name;
data.inboxDomain = $scope.email.inbox.domain;
}
Client.configureApp($scope.app.id, 'inbox', data, function (error) {
if (error && error.statusCode === 400) {
$scope.email.inboxBusy = false;
$scope.email.error.inboxName = error.message;
return;
}
if (error) return Client.error(error);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
// when the mailboxName is 'reset', this will fill it up with the default again
$scope.email.enableInbox = $scope.app.enableInbox ? true : false;
$scope.email.currentInbox = findInbox($scope.email.inboxes, $scope.app);
$scope.email.inbox = findInbox($scope.email.inboxes, $scope.app);
$timeout(function () { $scope.email.inboxBusy = false; }, 1000);
});
});
}
};
$scope.eventlog = {
busy: false,
eventLogs: [],
activeEventLog: null,
currentPage: 1,
perPage: 15,
show: function () {
$scope.eventlog.refresh();
},
refresh: function () {
$scope.eventlog.busy = true;
Client.getAppEventLog($scope.app.id, $scope.eventlog.currentPage, $scope.eventlog.perPage, function (error, result) {
if (error) return console.error('Failed to get events:', error);
$scope.eventlog.eventLogs = [];
result.forEach(function (e) {
$scope.eventlog.eventLogs.push({ raw: e, details: Client.eventLogDetails(e, $scope.app.id), source: Client.eventLogSource(e) });
});
$scope.eventlog.busy = false;
});
},
showDetails: function (eventLog) {
if ($scope.eventlog.activeEventLog === eventLog) $scope.eventlog.activeEventLog = null;
else $scope.eventlog.activeEventLog = eventLog;
},
showNextPage: function () {
$scope.eventlog.currentPage++;
$scope.eventlog.refresh();
},
showPrevPage: function () {
if ($scope.eventlog.currentPage > 1) $scope.eventlog.currentPage--;
else $scope.eventlog.currentPage = 1;
$scope.eventlog.refresh();
}
};
$scope.cron = {
busy: false,
error: {},
commonPatterns: [
{ value: '* * * * *', label: $translate.instant('app.cron.commonPattern.everyMinute') },
{ value: '0 * * * *', label: $translate.instant('app.cron.commonPattern.everyHour') },
{ value: '*/30 * * * *', label: $translate.instant('app.cron.commonPattern.twicePerHour') },
{ value: '0 0 * * *', label: $translate.instant('app.cron.commonPattern.everyDay') },
{ value: '0 */12 * * *', label: $translate.instant('app.cron.commonPattern.twicePerDay') },
{ value: '0 0 * * 0', label: $translate.instant('app.cron.commonPattern.everySunday') },
{ value: '@daily', label: $translate.instant('app.cron.commonPattern.daily') },
{ value: '@hourly', label: $translate.instant('app.cron.commonPattern.hourly') },
{ value: '@service', label: $translate.instant('app.cron.commonPattern.service') }
],
crontab: '',
crontabDefault: ''
+ '# +------------------------ minute (0 - 59)\n'
+ '# | +------------------- hour (0 - 23)\n'
+ '# | | +-------------- day of month (1 - 31)\n'
+ '# | | | +--------- month (1 - 12)\n'
+ '# | | | | +---- day of week (0 - 6) (Sunday=0 or 7)\n'
+ '# | | | | |\n'
+ '# * * * * * command to be executed\n\n',
show: function () {
$scope.cronForm.$setPristine();
$scope.cron.error = {};
$scope.cron.crontab = $scope.app.crontab;
if ($scope.cron.crontab === null) $scope.cron.crontab = $scope.cron.crontabDefault; // only when null, not when ''
},
submit: function () {
$scope.cron.error = {};
$scope.cron.busy = true;
Client.configureApp($scope.app.id, 'crontab', { crontab: $scope.cron.crontab }, function (error) {
if (error && error.statusCode === 400) {
$scope.cron.busy = false;
$scope.cron.error.crontab = error.message;
$scope.cronForm.$setPristine();
return;
}
if (error) return Client.error(error);
$scope.cronForm.$setPristine();
$timeout(function () { $scope.cron.busy = false; }, 1000);
});
},
addCommonPattern: function (pattern) {
$scope.cron.crontab += pattern + ' /path/to/command\n';
}
};
$scope.security = {
busy: false,
error: {},
success: false,
robotsTxt: '',
csp: '',
hstsPreload: false,
show: function () {
$scope.security.error = {};
$scope.security.robotsTxt = $scope.app.reverseProxyConfig.robotsTxt || '';
$scope.security.csp = $scope.app.reverseProxyConfig.csp || '';
$scope.security.hstsPreload = $scope.app.reverseProxyConfig.hstsPreload || false;
},
submit: function () {
$scope.security.busy = true;
$scope.security.error = {};
var reverseProxyConfig = {
robotsTxt: $scope.security.robotsTxt || null, // empty string resets
csp: $scope.security.csp || null, // empty string resets
hstsPreload: $scope.security.hstsPreload
};
Client.configureApp($scope.app.id, 'reverse_proxy', reverseProxyConfig, function (error) {
if (error) return Client.error(error);
$timeout(function () {
$scope.security.success = true;
$scope.security.busy = false;
}, 1000);
});
}
};
$scope.proxy = {
busy: false,
error: null,
success: false,
upstreamUri: '',
show: function () {
$scope.proxyForm.$setPristine();
$scope.proxy.error = null;
$scope.proxy.upstreamUri = $scope.app.upstreamUri || '';
},
submit: function () {
$scope.proxy.busy = true;
$scope.proxy.error = null;
var upstreamUri = $scope.proxy.upstreamUri.replace(/\/$/, '');
Client.configureApp($scope.app.id, 'upstream_uri', { upstreamUri: upstreamUri }, function (error) {
$scope.proxy.busy = false;
if (error && error.statusCode === 400) {
$scope.proxy.error = error.message;
$scope.proxyForm.$setPristine();
return;
}
if (error) return Client.error(error);
$scope.proxyForm.$setPristine();
$timeout(function () {
$scope.proxy.success = true;
}, 1000);
});
}
};
$scope.updates = {
busy: false,
busyCheck: false,
busyUpdate: false,
busyAutomaticUpdates: false,
skipBackup: false,
enableAutomaticUpdate: true,
show: function () {
$scope.updates.skipBackup = false;
$scope.updates.enableAutomaticUpdate = $scope.app.enableAutomaticUpdate;
},
toggleAutomaticUpdates: function () {
$scope.updates.busyAutomaticUpdates = true;
Client.configureApp($scope.app.id, 'automatic_update', { enable: !$scope.updates.enableAutomaticUpdate }, function (error) {
if (error) return Client.error(error);
refreshApp($scope.app.id, function (error) {
if (error) console.error(error);
$timeout(function () {
console.log($scope.updates.enableAutomaticUpdate, $scope.app.enableAutomaticUpdate);
$scope.updates.enableAutomaticUpdate = $scope.app.enableAutomaticUpdate;
$scope.updates.busyAutomaticUpdates = false;
}, 2000);
});
});
},
check: function () {
$scope.updates.busyCheck = true;
Client.checkForAppUpdates($scope.app.id, function (error) {
if (error) Client.error(error);
$scope.updates.busyCheck = false;
});
},
askUpdate: function () {
$scope.updates.busyUpdate = false;
$('#updateModal').modal('show');
},
confirmUpdate: function () {
$scope.updates.busyUpdate = true;
Client.updateApp($scope.app.id, $scope.config.update[$scope.app.id].manifest, { skipBackup: $scope.updates.skipBackup }, function (error) {
$scope.updates.busyUpdate = false;
if (error) return Client.error(error);
$('#updateModal').modal('hide');
refreshApp($scope.app.id);
});
}
};
$scope.backupDetails = {
backup: null,
show: function (backup) {
$scope.backupDetails.backup = backup;
$('#backupDetailsModal').modal('show');
}
};
$scope.backups = {
busy: false,
busyCreate: false,
busyAutomaticBackups: false,
error: {},
enableBackup: false,
backups: [],
createBackup: function () {
$scope.backups.busyCreate = true;
Client.backupApp($scope.app.id, function (error) {
if (error) Client.error(error);
refreshApp($scope.app.id, function () {
$scope.backups.busyCreate = false;
waitForAppTask(function (error) {
if (error) return Client.error(error);
$scope.backups.show(); // refresh backup listing
});
});
});
},
show: function () {
var app = $scope.app;
$scope.backups.error = {};
$scope.backups.enableBackup = app.enableBackup;
Client.getAppBackups(app.id, function (error, backups) {
if (error) return Client.error(error);
$scope.backups.backups = backups;
Client.getAppEventLog(app.id, 1, 1, function (error, result) {
if (error) return console.error('Failed to get events:', error);
if (result.length !== 0 && result[0].action == 'app.backup.finish') {
$scope.backups.error.message = result[0].data.errorMessage;
}
});
});
},
refresh: function () {
Client.getAppBackups($scope.app.id, function (error, backups) {
if (error) return Client.error(error);
$scope.backups.backups = backups;
});
},
toggleAutomaticBackups: function () {
$scope.backups.busyAutomaticBackups = true;
$scope.backups.error = {};
Client.configureApp($scope.app.id, 'automatic_backup', { enable: !$scope.backups.enableBackup }, function (error) {
if (error) return Client.error(error);
$timeout(function () {
$scope.backups.enableBackup = !$scope.backups.enableBackup;
$scope.backups.busyAutomaticBackups = false;
}, 1000);
});
}
};
$scope.s3like = function (provider) {
return provider === 's3' || provider === 'minio' || provider === 's3-v4-compat'
|| provider === 'exoscale-sos' || provider === 'digitalocean-spaces' || provider === 'hetzner-objectstorage'
|| provider === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' || provider === 'cloudflare-r2'
|| provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage' || provider === 'idrive-e2'
|| provider === 'contabo-objectstorage';
};
$scope.mountlike = function (provider) {
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
};
$scope.importBackup = {
busy: false,
error: {},
// variables here have to match the import config logic!
provider: '',
bucket: '',
prefix: '',
mountPoint: '', // for mountpoint
accessKeyId: '',
secretAccessKey: '',
gcsKey: { keyFileName: '', content: '' },
region: '',
endpoint: '',
acceptSelfSignedCerts: false,
format: 'tgz',
remotePath: '',
password: '',
encryptedFilenames: true,
mountOptions: {
host: '',
remoteDir: '',
username: '',
password: '',
diskPath: '',
user: '',
seal: true,
port: 22,
privateKey: ''
},
encrypted: false, // helps with ng-required when backupConfig is read from file
clearForm: function () {
// $scope.importBackup.provider = ''; // do not clear since we call this function on provider change
$scope.importBackup.bucket = '';
$scope.importBackup.mountPoint = '';
$scope.importBackup.accessKeyId = '';
$scope.importBackup.secretAccessKey = '';
$scope.importBackup.gcsKey.keyFileName = '';
$scope.importBackup.gcsKey.content = '';
$scope.importBackup.endpoint = '';
$scope.importBackup.region = '';
$scope.importBackup.format = 'tgz';
$scope.importBackup.acceptSelfSignedCerts = false;
$scope.importBackup.password = '';
$scope.importBackup.encryptedFilenames = true;
$scope.importBackup.remotePath = '';
$scope.importBackup.mountOptions = { host: '', remoteDir: '', username: '', password: '', diskPath: '', seal: true, user: '', port: 22, privateKey: '' };
},
submit: function () {
$scope.importBackup.error = {};
$scope.importBackup.busy = true;
var backupConfig = {
provider: $scope.importBackup.provider,
};
if ($scope.importBackup.password) {
backupConfig.password = $scope.importBackup.password;
backupConfig.encryptedFilenames = $scope.importBackup.encryptedFilenames;
}
var remotePath = $scope.importBackup.remotePath;
// 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 = backupConfig.region || 'us-east-1';
backupConfig.acceptSelfSignedCerts = $scope.importBackup.acceptSelfSignedCerts;
backupConfig.s3ForcePathStyle = true; // might want to expose this in the UI
} else if (backupConfig.provider === 'exoscale-sos') {
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'wasabi') {
backupConfig.region = $scope.wasabiRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
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';
} else if (backupConfig.provider === 'linode-objectstorage') {
backupConfig.region = $scope.linodeRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'ovh-objectstorage') {
backupConfig.region = $scope.ovhRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'ionos-objectstorage') {
backupConfig.region = $scope.ionosRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'vultr-objectstorage') {
backupConfig.region = $scope.vultrRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'contabo-objectstorage') {
backupConfig.region = $scope.contaboRegions.find(function (x) { return x.value === $scope.importBackup.endpoint; }).region;
backupConfig.signatureVersion = 'v4';
backupConfig.s3ForcePathStyle = true; // https://docs.contabo.com/docs/products/Object-Storage/technical-description (no virtual buckets)
} else if (backupConfig.provider === 'upcloud-objectstorage') {
var m = /^.*\.(.*)\.upcloudobjects.com$/.exec(backupConfig.endpoint);
backupConfig.region = m ? m[1] : 'us-east-1'; // let it fail in validation phase if m is not valid
backupConfig.signatureVersion = 'v4';
} else if (backupConfig.provider === 'digitalocean-spaces') {
backupConfig.region = 'us-east-1';
} else if (backupConfig.provider === 'hetzner-objectstorage') {
backupConfig.region = 'us-east-1';
backupConfig.signatureVersion = 'v4';
}
} 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 === 'sshfs' || backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.provider === 'ext4' || backupConfig.provider === 'xfs') {
backupConfig.mountOptions = $scope.importBackup.mountOptions;
backupConfig.prefix = $scope.importBackup.prefix;
} else if (backupConfig.provider === 'mountpoint') {
backupConfig.prefix = $scope.importBackup.prefix;
backupConfig.mountPoint = $scope.importBackup.mountPoint;
} else if (backupConfig.provider === 'filesystem') {
var parts = remotePath.split('/');
remotePath = parts.pop() || parts.pop(); // removes any trailing slash. this is basename()
backupConfig.backupFolder = parts.join('/'); // this is dirname()
}
if ($scope.importBackup.format === 'tgz') {
if (remotePath.substring(remotePath.length - '.tar.gz'.length, remotePath.length) === '.tar.gz') { // endsWith
remotePath = remotePath.replace(/.tar.gz$/, '');
} else if (remotePath.substring(remotePath.length - '.tar.gz.enc'.length, remotePath.length) === '.tar.gz.enc') { // endsWith
remotePath = remotePath.replace(/.tar.gz.enc$/, '');
}
}
Client.importBackup($scope.app.id, remotePath, $scope.importBackup.format, backupConfig, function (error) {
if (error) {
$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();
} else if (error.message.indexOf('not match the signature') !== -1 || error.message.indexOf('Signature') !== -1) {
$scope.importBackup.error.secretAccessKey = true;
$scope.importBackupForm.secretAccessKey.$setPristine();
$('#inputImportBackupSecretAccessKey').focus();
} else if (error.message.toLowerCase() === 'access denied') {
$scope.importBackup.error.accessKeyId = true;
$scope.importBackupForm.accessKeyId.$setPristine();
$('#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);
}
return;
}
$('#importBackupModal').modal('hide');
// clear potential post-install flag
$scope.app.pendingPostInstallConfirmation = false;
delete localStorage['confirmPostInstall_' + $scope.app.id];
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.importBackup.busy = false; }, 1000);
});
});
},
show: function () {
$scope.importBackup.clearForm();
$('#importBackupModal').modal('show');
},
};
$scope.editBackup = {
busy: false,
error: null,
backup: null,
label: '',
persist: false,
show: function (backup) {
$scope.editBackup.backup = backup;
$scope.editBackup.label = backup.label;
$scope.editBackup.persist = backup.preserveSecs === -1;
$scope.editBackup.error = null;
$scope.editBackup.busy = false;
$('#editBackupModal').modal('show');
},
submit: function () {
$scope.editBackup.error = null;
$scope.editBackup.busy = true;
Client.editAppBackup($scope.app.id, $scope.editBackup.backup.id, $scope.editBackup.label, $scope.editBackup.persist ? -1 : 0, function (error) {
$scope.editBackup.busy = false;
if (error) return $scope.editBackup.error = error.message;
$scope.backups.refresh();
$('#editBackupModal').modal('hide');
});
}
};
$scope.backupDetails = {
backup: null,
show: function (backup) {
$scope.backupDetails.backup = backup;
$('#backupDetailsModal').modal('show');
}
};
$scope.uninstall = {
busy: false,
error: {},
busyRunState: false,
startButton: false,
latestBackup: null,
toggleRunState: function (confirmStop) {
if (confirmStop && $scope.app.runState !== RSTATES.STOPPED) {
$('#stopModal').modal('show');
return;
}
$('#stopModal').modal('hide');
var func = $scope.app.runState === RSTATES.STOPPED ? Client.startApp : Client.stopApp;
$scope.uninstall.busyRunState = true;
func($scope.app.id, function (error) {
if (error) return Client.error(error);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.uninstall.busyRunState = false; }, 1000);
});
});
},
show: function () {
$scope.uninstall.error = {};
$scope.uninstall.latestBackup = null;
Client.getAppBackups($scope.app.id, function (error, backups) {
if (!error && backups.length) $scope.uninstall.latestBackup = backups[0];
});
},
ask: function (what) {
if (what === 'uninstall') {
$('#uninstallModal').modal('show');
} else {
$('#archiveModal').modal('show');
}
},
submit: function (what) {
$scope.uninstall.busy = true;
var NOOP = function (next) { return next(); };
var stopAppTask = $scope.app.taskId ? Client.stopTask.bind(null, $scope.app.taskId) : NOOP;
stopAppTask(function () { // ignore error
const func = what === 'uninstall' ?
Client.uninstallApp.bind(null, $scope.app.id) :
Client.archiveApp.bind(Client, $scope.app.id, $scope.uninstall.latestBackup.id);
func(function (error) {
if (error && error.statusCode === 402) { // unpurchase failed
Client.error('Relogin to Cloudron App Store');
} else if (error) {
Client.error(error);
} else {
if (what === 'uninstall') {
$('#uninstallModal').modal('hide');
} else {
$('#archiveModal').modal('hide');
}
$location.path('/apps');
}
$scope.uninstall.busy = false;
});
});
}
};
$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) {
$scope.restore.busy = false;
if (error) {
Client.error(error);
return;
}
$('#restoreModal').modal('hide');
refreshApp($scope.app.id);
});
}
};
$scope.clone = {
busy: false,
error: {},
backup: null,
subdomain: '',
domain: null,
secondaryDomains: {},
needsOverwrite: false,
overwriteDns: false,
ports: {},
portsEnabled: {},
portInfo: {},
show: function (backup) {
var app = $scope.app;
$scope.clone.error = {};
$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.needsOverwrite = false;
$scope.clone.overwriteDns = false;
$scope.clone.secondaryDomains = {};
var httpPorts = backup.manifest.httpPorts || {};
for (var env2 in httpPorts) {
$scope.clone.secondaryDomains[env2] = {
subdomain: httpPorts[env2].defaultValue || '',
domain: $scope.clone.domain
};
}
$scope.clone.portInfo = angular.extend({}, backup.manifest.tcpPorts, backup.manifest.udpPorts); // Portbinding map only for information
// set default ports
for (var env in $scope.clone.portInfo) {
$scope.clone.ports[env] = $scope.clone.portInfo[env].defaultValue || 0;
$scope.clone.portsEnabled[env] = true;
}
$('#appCloneModal').modal('show');
},
submit: function () {
$scope.clone.busy = true;
var secondaryDomains = {};
for (var env2 in $scope.clone.secondaryDomains) {
secondaryDomains[env2] = {
subdomain: $scope.clone.secondaryDomains[env2].subdomain,
domain: $scope.clone.secondaryDomains[env2].domain.domain
};
}
// only use enabled ports
var finalPorts = {};
for (var env in $scope.clone.ports) {
if ($scope.clone.portsEnabled[env]) {
finalPorts[env] = $scope.clone.ports[env];
}
}
var data = {
subdomain: $scope.clone.subdomain,
domain: $scope.clone.domain.domain,
secondaryDomains: secondaryDomains,
ports: finalPorts,
backupId: $scope.clone.backup.id,
overwriteDns: $scope.clone.overwriteDns
};
var allDomains = [{ domain: data.domain, subdomain: data.subdomain }].concat(Object.keys(secondaryDomains).map(function (k) {
return {
domain: secondaryDomains[k].domain,
subdomain: secondaryDomains[k].subdomain
};
}));
async.eachSeries(allDomains, function (domain, callback) {
if ($scope.clone.overwriteDns) return callback();
Client.checkDNSRecords(domain.domain, domain.subdomain, function (error, result) {
if (error) return callback(error);
var fqdn = domain.subdomain + '.' + domain.domain;
if (result.error) {
if (result.error.reason === ERROR.ACCESS_DENIED) return callback({ type: 'provider', fqdn: fqdn, message: 'DNS credentials for ' + domain.domain + ' are invalid. Update it in Domains & Certs view' });
return callback({ type: 'provider', fqdn: fqdn, message: result.error.message });
}
if (result.needsOverwrite) {
$scope.clone.needsOverwrite = true;
$scope.clone.overwriteDns = true;
return callback({ type: 'externally_exists', fqdn: fqdn, message: 'DNS Record already exists. Confirm that the domain is not in use for services external to Cloudron' });
}
callback();
});
}, function (error) {
if (error) {
if (error.type) {
$scope.clone.error.location = error;
$scope.clone.busy = false;
} else {
Client.error(error);
}
$scope.clone.error.location = error;
$scope.clone.busy = false;
return;
}
Client.cloneApp($scope.app.id, data, function (error/*, clonedApp */) {
$scope.clone.busy = false;
if (error) {
var errorMessage = error.message.toLowerCase();
if (errorMessage.indexOf('port') !== -1) {
$scope.clone.error.port = error.message;
} else if (error.message.indexOf('location') !== -1 || error.message.indexOf('subdomain') !== -1) {
// TODO extract fqdn from error message, currently we just set it always to the main location
$scope.clone.error.location = { type: 'internally_exists', fqdn: data.subdomain + '.' + data.domain, message: error.message };
$('#cloneLocationInput').focus();
} else {
Client.error(error);
}
return;
}
$('#appCloneModal').modal('hide');
$location.path('/apps');
});
});
}
};
$scope.repair = {
retryBusy: false,
error: {},
subdomain: null,
domain: null,
redirectDomains: [],
aliasDomains: [],
backups: [],
backupId: '',
show: function () {},
// this prepares the repair dialog with whatever is required for repair action
confirm: function () {
$scope.repair.error = {};
$scope.repair.retryBusy = false;
$scope.repair.subdomain = null;
$scope.repair.domain = null;
$scope.repair.redirectDomains = [];
$scope.repair.aliasDomains = [];
$scope.repair.backupId = '';
var app = $scope.app;
var errorState = ($scope.app.error && $scope.app.error.installationState) || ISTATES.PENDING_CONFIGURE;
if (errorState === ISTATES.PENDING_LOCATION_CHANGE) {
$scope.repair.subdomain = app.subdomain;
$scope.repair.domain = $scope.domains.filter(function (d) { return d.domain === app.domain; })[0];
$scope.repair.aliasDomains = $scope.app.aliasDomains;
$scope.repair.aliasDomains = $scope.app.aliasDomains.map(function (aliasDomain) {
return {
subdomain: aliasDomain.subdomain,
enabled: true,
domain: $scope.domains.filter(function (d) { return d.domain === aliasDomain.domain; })[0]
};
});
$scope.repair.redirectDomains = $scope.app.redirectDomains;
$scope.repair.redirectDomains = $scope.app.redirectDomains.map(function (altDomain) {
return {
subdomain: altDomain.subdomain,
enabled: true,
domain: $scope.domains.filter(function (d) { return d.domain === altDomain.domain; })[0]
};
});
}
if (errorState === ISTATES.PENDING_RESTORE || errorState === ISTATES.PENDING_IMPORT) {
Client.getAppBackups($scope.app.id, function (error, backups) {
if (error) return Client.error(error);
$scope.repair.backups = backups;
$scope.repair.backupId = '';
$('#repairModal').modal('show');
});
return;
}
$('#repairModal').modal('show');
},
submit: function () {
$scope.repair.error = {};
$scope.repair.retryBusy = true;
var errorState = ($scope.app.error && $scope.app.error.installationState) || ISTATES.PENDING_CONFIGURE;
var data = {};
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:
data.subdomain = $scope.repair.subdomain;
data.domain = $scope.repair.domain.domain;
data.aliasDomains = $scope.repair.aliasDomains.filter(function (a) { return a.enabled; })
.map(function (d) { return { subdomain: d.subdomain, domain: d.domain.domain }; });
data.redirectDomains = $scope.repair.redirectDomains.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
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, 'storage', { storageVolumeId: null, storageVolumePrefix: 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:
case ISTATES.PENDING_IMPORT:
if ($scope.repair.backups.length === 0) { // this can happen when you give some invalid backup via CLI and restore via UI
repairFunc = Client.repairApp.bind(null, $scope.app.id, {}); // this will trigger a re-install
} else {
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:
case ISTATES.PENDING_RESTART:
case ISTATES.PENDING_RESIZE:
case ISTATES.PENDING_DEBUG:
case ISTATES.PENDING_RECREATE_CONTAINER:
case ISTATES.PENDING_CONFIGURE:
case ISTATES.PENDING_BACKUP: // can happen if the backup task was killed/rebooted
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;
}
repairFunc(function (error) {
$scope.repair.retryBusy = false;
if (error) return Client.error(error);
$scope.repair.retryBusy = false;
$('#repairModal').modal('hide');
});
},
restartBusy: false,
restartApp: function () {
$scope.repair.restartBusy = true;
Client.restartApp($scope.app.id, function (error) {
if (error) return console.error(error);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.repair.restartBusy = false; }, 1000);
});
});
},
pauseBusy: false,
pauseAppBegin: function () {
$scope.repair.pauseBusy = true;
Client.debugApp($scope.app.id, true, function (error) {
if (error) return console.error(error);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.repair.pauseBusy = false; }, 1000);
});
});
},
pauseAppDone: function () {
$scope.repair.pauseBusy = true;
Client.debugApp($scope.app.id, false, function (error) {
if (error) return console.error(error);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.repair.pauseBusy = false; }, 1000);
});
});
}
};
function fetchUsers(callback) {
Client.getAllUsers(function (error, users) {
if (error) return callback(error);
$scope.users = users;
callback();
});
}
function fetchGroups(callback) {
Client.getGroups(function (error, groups) {
if (error) return callback(error);
$scope.groups = groups;
callback();
});
}
function fetchDiskUsage(callback) {
$scope.diskUsage = -1;
$scope.diskUsageDate = 0;
Client.diskUsage(function (error, result) {
if (error) return callback(error);
if (!result.usage) return callback(); // no usage date yet
$scope.diskUsageDate = result.usage.ts;
for (var diskName in result.usage.disks) {
var disk = result.usage.disks[diskName];
var content = disk.contents.find(function (c) { return c.id === appId; });
if (content) {
$scope.diskUsage = content.usage;
break;
}
}
callback();
});
}
function getDomains(callback) {
Client.getDomains(function (error, result) {
if (error) return callback(error);
$scope.domains = result;
callback();
});
}
function getVolumes(callback) {
Client.getVolumes(function (error, result) {
if (error) return callback(error);
$scope.volumes = result;
callback();
});
}
function getBackupConfig(callback) {
Client.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
$scope.backupConfig = backupConfig;
callback();
});
}
function refreshApp(appId, callback) {
callback = callback || function () {};
Client.getAppWithTask(appId, function (error, app) {
if (error && error.statusCode === 404) return $location.path('/apps');
if (error) return callback(error);
$scope.app = app;
// show 'Start App' if app is starting or is stopped
if (app.installationState === ISTATES.PENDING_START || app.installationState === ISTATES.PENDING_STOP) {
$scope.uninstall.startButton = app.installationState === ISTATES.PENDING_START;
} else {
$scope.uninstall.startButton = app.runState === RSTATES.STOPPED;
}
callback();
});
}
function waitForAppTask(callback) {
callback = callback || function () {};
if (!$scope.app.taskId) return callback();
// app will be refreshed on interval
$timeout(waitForAppTask.bind(null, callback), 2000); // not yet done
}
// https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
$scope.downloadConfig = function (backup) {
// secrets and tokens already come with placeholder characters we remove them
var tmp = {
remotePath: backup.remotePath,
encrypted: !!$scope.backupConfig.password // we add this just to help the import UI
};
Object.keys($scope.backupConfig).forEach(function (k) {
var v = $scope.backupConfig[k];
if (v && typeof v === 'object') { // to hide mountOptions.password and the likes
tmp[k] = {};
Object.keys(v).forEach(function (j) {
if (v[j] !== SECRET_PLACEHOLDER) tmp[k][j] = v[j];
});
} else {
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = v;
}
});
var filename = 'app-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + ' (' + $scope.app.fqdn + ')' + '.json';
download(filename, JSON.stringify(tmp, null, 4));
};
document.getElementById('backupConfigFileInput').onchange = function (event) {
var reader = new FileReader();
reader.onload = function (result) {
if (!result.target || !result.target.result) return console.error('Unable to read backup config');
var backupConfig;
try {
backupConfig = JSON.parse(result.target.result);
if (backupConfig.provider === 'filesystem') { // this allows a user to upload a backup to server and import easily with an absolute path
backupConfig.remotePath = backupConfig.backupFolder + '/' + backupConfig.remotePath;
delete backupConfig.backupFolder;
}
} catch (e) {
console.error('Unable to parse backup config', e);
return;
}
$scope.$apply(function () {
// we assume property names match here, this does not yet work for gcs keys
Object.keys(backupConfig).forEach(function (k) {
if (k in $scope.importBackup) {
$scope.importBackup[k] = backupConfig[k];
}
});
});
};
reader.readAsText(event.target.files[0]);
};
Client.onReady(function () {
refreshApp(appId, function (error) {
if (error) return Client.error(error);
if ($scope.app.accessLevel !== 'admin' && $scope.app.accessLevel !== 'operator') return $location.path('/');
// 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' : 'info', true /* skipViewShow */);
}
function done() {
$scope[$scope.view].show(); // initialize now that we have all the values
var refreshTimer = $interval(function () { refreshApp($scope.app.id); }, 5000); // call with inline function to avoid iteration argument passed see $interval docs
$scope.$on('$destroy', function () {
$interval.cancel(refreshTimer);
});
}
if ($scope.app.accessLevel !== 'admin') return done();
async.series([
fetchUsers,
fetchGroups,
fetchDiskUsage,
getDomains,
getVolumes,
getBackupConfig
], function (error) {
if (error) return Client.error(error);
// check for updates, if the app has a pending update. this handles two cases:
// 1. user got a valid subscription. this will make the updates get the manifest field
// 2. user has not refreshed the ui in a while or updated via cli tool. this will ensure we are not holding to a dangling update
if ($scope.config.update[$scope.app.id]) Client.checkForUpdates();
done();
});
});
});
$('#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]);
};
// setup all the dialog focus handling
['appUninstallModal', 'appUpdateModal', 'appRestoreModal', 'appCloneModal', 'editBackupModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
$(this).find('[autofocus]:first').focus();
});
});
var clipboard = new Clipboard('.clipboard');
clipboard.on('success', function () {
$scope.$apply(function () { $scope.copyBackupIdDone = true; });
$timeout(function () { $scope.copyBackupIdDone = false; }, 5000);
});
$('.modal-backdrop').remove();
}]);