Files
cloudron-box/src/views/app.js

1566 lines
60 KiB
JavaScript
Raw Normal View History

'use strict';
/* global angular */
/* global $ */
/* global asyncSeries */
/* 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 */
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('/'); });
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' }
];
// 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();
$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: /';
$scope.setView = function (view, skipViewShow) {
2019-09-17 14:52:22 +02:00
if ($scope.view === view) return;
$route.updateParams({ view: view });
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);
});
},
$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.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 = {};
// 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);
});
2019-09-12 16:28:21 +02: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) {
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
2019-09-12 16:28:21 +02:00
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);
2019-09-12 16:28:21 +02:00
done();
});
});
});
}
};
$scope.location = {
busy: false,
error: {},
domainCollisions: [],
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 = {};
$scope.location.domainCollisions = [];
$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;
}
}
},
submit: function (overwriteDns) {
$('#domainCollisionsModal').modal('hide');
$scope.location.busy = true;
$scope.location.error = {};
$scope.location.domainCollisions = [];
// 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 = {
overwriteDns: !!overwriteDns,
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 };})
};
// 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();
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) {
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;
} else {
2019-09-23 23:47:33 +02:00
$scope.location.error.alternateDomains = domain.domain + ' ' + result.error.message;
}
$scope.location.busy = false;
return;
}
2019-09-23 23:47:33 +02:00
if (result.needsOverwrite) $scope.location.domainCollisions.push(domain);
callback();
});
}, function (error) {
if (error) {
$scope.location.busy = false;
return Client.error(error);
}
if ($scope.location.domainCollisions.length) {
$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)) {
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;
}
$scope.location.busy = false;
return;
}
if (error) return Client.error(error);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
2019-09-11 21:24:25 +02:00
$scope.locationForm.$setPristine();
$timeout(function () { $scope.location.busy = false; }, 1000);
});
});
});
}
};
$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 = {};
$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);
$timeout(function () {
$scope.access.success = true;
$scope.access.busy = false;
}, 1000);
});
}
};
$scope.resources = {
error: {},
2020-04-29 22:18:44 -07:00
busy: false,
currentMemoryLimit: 0,
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,
dataDir: null,
2020-04-29 22:18:44 -07:00
busyBinds: false,
binds: [],
show: function () {
var app = $scope.app;
2019-09-19 18:31:11 +02:00
$scope.resources.error = {};
$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;
$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 });
});
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-18 17:12:10 +02:00
submitMemoryLimit: function () {
$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;
Client.configureApp($scope.app.id, 'memory_limit', { memoryLimit: memoryLimit }, function (error) {
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);
});
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 = {};
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();
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
$timeout(function () { $scope.resources.busyDataDir = false; }, 1000);
});
});
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);
});
});
}
};
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');
});
}
};
$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: '',
show: function () {
var app = $scope.app;
$scope.emailForm.$setPristine();
2019-09-19 18:31:11 +02:00
$scope.email.error = {};
$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;
},
submit: function () {
2019-09-17 15:09:39 +02:00
$scope.email.error = {};
$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
$scope.emailForm.$setPristine();
2019-12-16 16:08:49 -08:00
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.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;
$timeout(function () { $scope.email.busy = false; }, 1000);
});
2019-09-17 15:09:39 +02:00
});
}
};
$scope.security = {
busy: false,
error: {},
success: false,
robotsTxt: '',
2019-10-14 16:50:15 -07:00
csp: '',
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 || '';
},
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);
$timeout(function () {
$scope.security.success = true;
$scope.security.busy = false;
}, 1000);
});
}
};
$scope.updates = {
busy: false,
2019-09-17 16:16:48 +02:00
busyCheck: false,
busyUpdate: false,
skipBackup: false,
enableAutomaticUpdate: false,
show: function () {
var app = $scope.app;
$scope.updates.enableAutomaticUpdate = app.enableAutomaticUpdate;
},
2019-09-17 16:16:48 +02:00
toggleAutomaticUpdates: function () {
$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) {
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;
});
},
askUpdate: function () {
$scope.updates.busyUpdate = false;
$('#updateModal').modal('show');
},
confirmUpdate: function () {
$scope.updates.busyUpdate = true;
Client.updateApp($scope.app.id, $scope.config.update.apps[$scope.app.id].manifest, { skipBackup: $scope.updates.skipBackup }, function (error) {
$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);
});
}
};
$scope.backups = {
busy: false,
2019-09-20 00:03:52 +02:00
busyCreate: false,
error: {},
copyBackupIdDone: false,
enableBackup: false,
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
});
},
show: function () {
var app = $scope.app;
2019-09-19 18:31:11 +02:00
$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;
});
},
2019-09-17 16:16:48 +02:00
toggleAutomaticBackups: function () {
$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) {
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);
});
}
};
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-02-06 16:08:22 -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');
}
};
$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 () {
$('#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-29 16:52:57 -07:00
$scope.uninstall.busy = false;
});
});
}
};
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-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
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
2019-12-16 16:27:24 -08:00
$timeout(function () { $scope.console.busyRunState = false; }, 1000);
});
2019-09-23 15:50:41 -07: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
$scope.repair = {
retryBusy: false,
error: {},
location: null,
domain: null,
alternateDomains: [],
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 () {
$scope.repair.error = {};
$scope.repair.retryBusy = false;
$scope.repair.location = null;
$scope.repair.domain = null;
$scope.repair.alternateDomains = [];
2019-09-23 15:01:44 +02:00
$scope.repair.backupId = '';
2019-09-23 15:01:44 +02:00
var app = $scope.app;
var errorState = ($scope.app.error && $scope.app.error.installationState) || ISTATES.PENDING_CONFIGURE;
if (errorState === ISTATES.PENDING_LOCATION_CHANGE) {
$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;
$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
});
}
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-23 15:01:44 +02:00
$('#repairModal').modal('show');
});
return;
2019-09-23 15:01:44 +02:00
}
$('#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.location = $scope.repair.location;
data.domain = $scope.repair.domain.domain;
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
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:
case ISTATES.PENDING_RESTART:
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
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) {
2020-03-31 17:45:34 -07:00
$scope.repair.retryBusy = false;
if (error) return Client.error(error);
$scope.repair.retryBusy = false;
$('#repairModal').modal('hide');
});
2019-12-16 13:30:51 -08:00
},
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);
});
});
},
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);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
2019-12-16 18:18:22 -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);
refreshApp($scope.app.id, function (error) {
if (error) return Client.error(error);
2019-12-16 18:18:22 -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-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');
}
};
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) {
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) {
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;
}
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();
});
} 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-24 21:27:49 +02:00
callback();
}
2019-09-12 16:28:21 +02:00
});
}
function waitForAppTask(callback) {
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();
// app will be refreshed on interval
$timeout(waitForAppTask.bind(null, callback), 2000); // not yet done
2019-09-11 21:24:25 +02:00
}
Client.onReady(function () {
2019-12-16 16:22:29 -08:00
refreshApp(appId, function (error) {
if (error) return Client.error(error);
// 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 */);
}
asyncSeries([
fetchUsers,
fetchGroups,
getDomains,
getBackupConfig
], function (error) {
if (error) return Client.error(error);
$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
$scope.$on('$destroy', function () {
$interval.cancel(refreshTimer);
});
});
});
});
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]);
};
// setup all the dialog focus handling
2019-09-12 16:28:21 +02:00
['appUninstallModal', 'appUpdateModal', 'appRestoreModal'].forEach(function (id) {
$('#' + id).on('shown.bs.modal', function () {
2019-09-23 10:16:19 -07:00
$(this).find('[autofocus]:first').focus();
});
});
$('.modal-backdrop').remove();
}]);