1852 lines
73 KiB
JavaScript
1852 lines
73 KiB
JavaScript
'use strict';
|
|
|
|
/* global angular */
|
|
/* global $ */
|
|
/* global async */
|
|
/* global RSTATES */
|
|
/* global ISTATES */
|
|
/* global ERROR */
|
|
/* global Chart */
|
|
/* global Clipboard */
|
|
/* global SECRET_PLACEHOLDER */
|
|
|
|
angular.module('Application').controller('AppController', ['$scope', '$location', '$timeout', '$interval', '$route', '$routeParams', 'Client', function ($scope, $location, $timeout, $interval, $route, $routeParams, Client) {
|
|
// 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: 'China (Beijing)', value: 'cn-north-1' },
|
|
{ name: 'China (Ningxia)', value: 'cn-northwest-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.us-east-1.wasabisys.com' },
|
|
{ name: 'US East 2', value: 'https://s3.us-east-2.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: 'SFO3', value: 'https://sfo3.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' }
|
|
];
|
|
|
|
$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' },
|
|
{ name: 'Singapore', value: 'ap-south-1.linodeobjects.com', region: 'us-east-1' },
|
|
];
|
|
|
|
// note: ovh also has a storage endpoint but that only supports path style access
|
|
$scope.ovhRegions = [
|
|
{ name: 'Beauharnois (BHS)', value: 'https://s3.bhs.cloud.ovh.net', region: 'bhs' }, // default
|
|
{ name: 'Frankfurt (DE)', value: 'https://s3.de.cloud.ovh.net', region: 'de' },
|
|
{ name: 'Gravelines (GRA)', value: 'https://s3.gra.cloud.ovh.net', region: 'gra' },
|
|
{ name: 'Strasbourg (SBG)', value: 'https://s3.sbg.cloud.ovh.net', region: 'sbg' },
|
|
{ name: 'London (UK)', value: 'https://s3.uk.cloud.ovh.net', region: 'uk' },
|
|
{ name: 'Sydney (SYD)', value: 'https://s3.syd.cloud.ovh.net', region: 'syd' },
|
|
{ name: 'Warsaw (WAW)', value: 'https://s3.waw.cloud.ovh.net', region: 'waw' },
|
|
];
|
|
|
|
// https://devops.ionos.com/api/s3/
|
|
$scope.ionosRegions = [
|
|
{ name: 'DE', value: 'https://s3-de-central.profitbricks.com', region: 's3-de-central' }, // default
|
|
];
|
|
|
|
$scope.vultrRegions = [
|
|
{ name: 'New Jersey', value: 'https://ewr1.vultrobjects.com', region: 'us-east-1' }, // default
|
|
];
|
|
|
|
$scope.storageProvider = [
|
|
{ name: 'Amazon S3', value: 's3' },
|
|
{ name: 'Backblaze B2 (S3 API)', value: 'backblaze-b2' },
|
|
{ name: 'CIFS Mount', value: 'cifs' },
|
|
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces' },
|
|
{ name: 'Exoscale SOS', value: 'exoscale-sos' },
|
|
// { name: 'EXT4', value: 'ext4' },
|
|
{ name: 'Filesystem', value: 'filesystem' },
|
|
// { name: 'Filesystem (Mountpoint)', value: 'mountpoint' },
|
|
{ name: 'Google Cloud Storage', value: 'gcs' },
|
|
{ name: 'IONOS (Profitbricks)', value: 'ionos-objectstorage' },
|
|
{ name: 'Linode Object Storage', value: 'linode-objectstorage' },
|
|
{ name: 'Minio', value: 'minio' },
|
|
{ name: 'NFS Mount', value: 'nfs' },
|
|
{ name: 'OVH Object Storage', value: 'ovh-objectstorage' },
|
|
{ name: 'S3 API Compatible (v4)', value: 's3-v4-compat' },
|
|
{ name: 'Scaleway Object Storage', value: 'scaleway-objectstorage' },
|
|
{ name: 'SSHFS Mount', value: 'sshfs' },
|
|
{ name: 'UpCloud Object Storage', value: 'upcloud-objectstorage' },
|
|
{ name: 'Vultr Object Storage', value: 'vultr-objectstorage' },
|
|
// { name: 'No-op (Only for testing)', value: 'noop' },
|
|
{ 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();
|
|
|
|
// note: these variables will remain empty for operators
|
|
$scope.domains = [];
|
|
$scope.volumes = [];
|
|
$scope.groups = [];
|
|
$scope.users = [];
|
|
$scope.backupConfig = null;
|
|
|
|
$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.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.sftpInfo = {
|
|
show: function () {
|
|
$('#sftpInfoModal').modal('show');
|
|
}
|
|
};
|
|
|
|
$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,
|
|
location: '',
|
|
alternateDomains: [],
|
|
aliasDomains: [],
|
|
portBindings: {},
|
|
portBindingsEnabled: {},
|
|
portBindingsInfo: {},
|
|
|
|
addAlternateDomain: function (event) {
|
|
event.preventDefault();
|
|
$scope.location.alternateDomains.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('alternateDomainsInput-' + ($scope.location.alternateDomains.length-1)).focus();
|
|
}, 200);
|
|
},
|
|
|
|
delAlternateDomain: function (event, index) {
|
|
event.preventDefault();
|
|
$scope.location.alternateDomains.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.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] };});
|
|
$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.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 };}),
|
|
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.location !== data.location) domains.push({ subdomain: data.location, domain: data.domain, type: 'main' });
|
|
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, 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' });
|
|
});
|
|
|
|
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 === 'main') {
|
|
$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.alternateDomains = domain.domain + ' ' + result.error.message;
|
|
}
|
|
$scope.location.busy = false;
|
|
return;
|
|
}
|
|
|
|
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)) {
|
|
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 { // FIXME: check error in aliasDomains
|
|
$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);
|
|
|
|
$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['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;
|
|
$scope.access.success = true;
|
|
$scope.access.busy = false;
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.resources = {
|
|
error: {},
|
|
|
|
busy: false,
|
|
currentMemoryLimit: 0,
|
|
memoryLimit: 0,
|
|
memoryTicks: [],
|
|
|
|
busyCpuShares: false,
|
|
currentCpuShares: 0,
|
|
cpuShares: 0,
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.resources.error = {};
|
|
$scope.resources.currentMemoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
|
|
$scope.resources.memoryLimit = $scope.resources.currentMemoryLimit;
|
|
$scope.resources.currentCpuShares = $scope.resources.cpuShares = app.cpuShares;
|
|
|
|
Client.getAppLimits(app.id, function (error, limits) {
|
|
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(limits.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);
|
|
}
|
|
});
|
|
},
|
|
|
|
submitMemoryLimit: function () {
|
|
$scope.resources.busy = true;
|
|
$scope.resources.error = {};
|
|
|
|
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 && 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);
|
|
});
|
|
});
|
|
},
|
|
|
|
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);
|
|
});
|
|
});
|
|
},
|
|
};
|
|
|
|
$scope.storage = {
|
|
error: {},
|
|
|
|
busy: false,
|
|
|
|
busyDataDir: false,
|
|
dataDir: null,
|
|
|
|
busyBinds: false,
|
|
mounts: [], // { volume, readOnly }
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.storage.error = {};
|
|
$scope.storage.dataDir = app.dataDir;
|
|
$scope.storage.mounts = [];
|
|
|
|
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 });
|
|
});
|
|
},
|
|
|
|
submitDataDir: function () {
|
|
$scope.storage.busyDataDir = true;
|
|
$scope.storage.error = {};
|
|
|
|
Client.configureApp($scope.app.id, 'data_dir', { dataDir: $scope.storage.dataDir || null }, function (error) {
|
|
if (error && error.statusCode === 400) {
|
|
$scope.storage.error.dataDir = error.message;
|
|
$scope.storage.busyDataDir = false;
|
|
return;
|
|
}
|
|
if (error) return Client.error(error);
|
|
|
|
$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 });
|
|
});
|
|
|
|
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: {},
|
|
|
|
period: 12, // set as 12 because disk graphs is only collected twice a day
|
|
memoryChart: null,
|
|
diskChart: null,
|
|
|
|
setPeriod: function (hours) {
|
|
$scope.graphs.period = hours;
|
|
$scope.graphs.show();
|
|
},
|
|
|
|
show: function () {
|
|
// both in minutes
|
|
var timePeriod = $scope.graphs.period * 60;
|
|
var timeBucketSize = $scope.graphs.period > 24 ? (6*60) : 5;
|
|
|
|
function fillGraph(canvasId, data, label, chartPropertyName, max) {
|
|
if (!data) return; // no data available yet
|
|
|
|
// fill holes with previous value
|
|
var cur = 0;
|
|
data.datapoints.forEach(function (d) {
|
|
if (d[0] === null) d[0] = cur;
|
|
else cur = d[0];
|
|
});
|
|
|
|
// translate the data from bytes to MB
|
|
var datapoints = data.datapoints.map(function (d) { return parseInt((d[0] / 1024 / 1024).toFixed(2)); });
|
|
|
|
// we calculate the labels based on timePeriod of chart / datapoints
|
|
var minuteSteps = timePeriod / datapoints.length;
|
|
var labels = datapoints.map(function (d, index) {
|
|
var dateTime = new Date(Date.now() - ((timePeriod - (index * minuteSteps)) * 60 * 1000));
|
|
|
|
if ($scope.graphs.period > 24) {
|
|
return dateTime.toLocaleDateString();
|
|
} else {
|
|
return dateTime.toLocaleTimeString();
|
|
}
|
|
});
|
|
|
|
var graphData = {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: label,
|
|
backgroundColor: '#82C4F844',
|
|
borderColor: '#2196F3',
|
|
borderWidth: 1,
|
|
radius: 0,
|
|
data: datapoints
|
|
}]
|
|
};
|
|
|
|
var options = {
|
|
maintainAspectRatio: true,
|
|
aspectRatio: 2.5,
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltips: {
|
|
intersect: false
|
|
},
|
|
scales: {
|
|
xAxes: [{
|
|
ticks: {
|
|
autoSkipPadding: 20,
|
|
}
|
|
}],
|
|
yAxes: [{
|
|
ticks: {
|
|
min: 0,
|
|
max: max,
|
|
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: graphData, options: options });
|
|
}
|
|
|
|
var memoryQuery = 'summarize(sum(collectd.localhost.table-' + appId + '-memory.gauge-rss, collectd.localhost.table-' + appId + '-memory.gauge-swap), "' + timeBucketSize + 'min", "avg")';
|
|
|
|
Client.graphs([ memoryQuery ], '-' + timePeriod + 'min', { appId: appId }, function (error, result) {
|
|
if (error) return console.error(error);
|
|
|
|
var currentMemoryLimit = $scope.app.memoryLimit || $scope.app.manifest.memoryLimit || (256 * 1024 * 1024);
|
|
|
|
fillGraph('#graphsMemoryChart', result[0], 'Memory', 'memoryChart', currentMemoryLimit / 1024 / 1024);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.email = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
enableMailbox: true,
|
|
mailboxName: '',
|
|
mailboxDomain: '',
|
|
currentMailboxName: '',
|
|
currentMailboxDomainName: '',
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.emailForm.$setPristine();
|
|
$scope.email.error = {};
|
|
$scope.email.enableMailbox = app.enableMailbox ? '1' : '0';
|
|
$scope.email.mailboxName = app.mailboxName || '';
|
|
$scope.email.mailboxDomain = $scope.domains.filter(function (d) { return d.domain === app.mailboxDomain; })[0];
|
|
$scope.email.currentMailboxName = app.mailboxName || '';
|
|
$scope.email.currentMailboxDomainName = $scope.email.mailboxDomain.domain;
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.email.error = {};
|
|
$scope.email.busy = true;
|
|
|
|
Client.configureApp($scope.app.id, 'mailbox', { enable: $scope.email.enableMailbox === '1', mailboxName: $scope.email.mailboxName || null, mailboxDomain: $scope.email.mailboxDomain.domain }, function (error) {
|
|
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);
|
|
|
|
$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; })[0];
|
|
$scope.email.currentMailboxName = $scope.app.mailboxName || '';
|
|
$scope.email.currentMailboxDomainName = $scope.email.mailboxDomain.domain;
|
|
|
|
$timeout(function () { $scope.email.busy = false; }, 1000);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.eventlog = {
|
|
busy: false,
|
|
eventlogs: [],
|
|
activeEventLog: null,
|
|
currentPage: 1,
|
|
perPage: 30,
|
|
|
|
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), 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: {},
|
|
|
|
crontab: '',
|
|
|
|
show: function () {
|
|
$scope.cronForm.$setPristine();
|
|
$scope.cron.error = {};
|
|
$scope.cron.crontab = $scope.app.crontab;
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.cron.error = {};
|
|
$scope.cron.busy = true;
|
|
|
|
Client.configureApp($scope.app.id, 'cron', { 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);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.security = {
|
|
busy: false,
|
|
error: {},
|
|
success: false,
|
|
|
|
robotsTxt: '',
|
|
csp: '',
|
|
|
|
show: function () {
|
|
$scope.security.error = {};
|
|
$scope.security.robotsTxt = $scope.app.reverseProxyConfig.robotsTxt || '';
|
|
$scope.security.csp = $scope.app.reverseProxyConfig.csp || '';
|
|
},
|
|
|
|
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
|
|
};
|
|
|
|
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,
|
|
busyCheck: false,
|
|
busyUpdate: false,
|
|
busyAutomaticUpdates: false,
|
|
skipBackup: false,
|
|
|
|
enableAutomaticUpdate: false,
|
|
|
|
show: function () {
|
|
var app = $scope.app;
|
|
|
|
$scope.updates.enableAutomaticUpdate = app.enableAutomaticUpdate;
|
|
$scope.updates.skipBackup = !app.enableAutomaticUpdate && !app.enableBackup;
|
|
},
|
|
|
|
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);
|
|
|
|
$timeout(function () {
|
|
$scope.updates.enableAutomaticUpdate = !$scope.updates.enableAutomaticUpdate;
|
|
$scope.updates.busyAutomaticUpdates = false;
|
|
}, 1000);
|
|
});
|
|
},
|
|
|
|
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.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;
|
|
});
|
|
},
|
|
|
|
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 === 'scaleway-objectstorage' || provider === 'wasabi' || provider === 'backblaze-b2' ||
|
|
provider === 'linode-objectstorage' || provider === 'ovh-objectstorage' || provider === 'ionos-objectstorage'
|
|
|| provider === 'vultr-objectstorage' || provider === 'upcloud-objectstorage';
|
|
};
|
|
|
|
$scope.mountlike = function (provider) {
|
|
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs';
|
|
};
|
|
|
|
$scope.importBackup = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
// variables here have to match the import config logic!
|
|
provider: '',
|
|
bucket: '',
|
|
prefix: '',
|
|
mountPoint: '',
|
|
accessKeyId: '',
|
|
secretAccessKey: '',
|
|
gcsKey: { keyFileName: '', content: '' },
|
|
region: '',
|
|
endpoint: '',
|
|
acceptSelfSignedCerts: false,
|
|
format: 'tgz',
|
|
backupId: '',
|
|
password: '',
|
|
mountOptions: {},
|
|
|
|
clearForm: function () {
|
|
// $scope.importBackup.provider = ''; // do not clear since we call this function on provider change
|
|
$scope.importBackup.bucket = '';
|
|
$scope.importBackup.prefix = '';
|
|
$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.backupId = '';
|
|
$scope.importBackup.mountOptions = {};
|
|
},
|
|
|
|
submit: function () {
|
|
$scope.importBackup.error = {};
|
|
$scope.importBackup.busy = true;
|
|
|
|
var backupConfig = {
|
|
provider: $scope.importBackup.provider,
|
|
};
|
|
if ($scope.importBackup.password) backupConfig.password = $scope.importBackup.password;
|
|
|
|
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 = 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 === '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 === '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.mountOptions = $scope.importBackup.mountOptions;
|
|
backupConfig.prefix = $scope.importBackup.prefix;
|
|
} 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()
|
|
}
|
|
|
|
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$/, '');
|
|
}
|
|
}
|
|
|
|
Client.importBackup($scope.app.id, backupId, $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');
|
|
|
|
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.uninstall = {
|
|
busy: false,
|
|
error: {},
|
|
busyRunState: false,
|
|
startButton: false,
|
|
|
|
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 = {};
|
|
},
|
|
|
|
ask: function () {
|
|
$('#uninstallModal').modal('show');
|
|
},
|
|
|
|
submit: function () {
|
|
$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
|
|
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');
|
|
$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) {
|
|
if (error) {
|
|
Client.error(error);
|
|
$scope.restore.busy = false;
|
|
return;
|
|
}
|
|
|
|
$('#restoreModal').modal('hide');
|
|
|
|
refreshApp($scope.app.id);
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.clone = {
|
|
busy: false,
|
|
error: {},
|
|
|
|
backup: null,
|
|
location: '',
|
|
domain: null,
|
|
portBindings: {},
|
|
portBindingsInfo: {},
|
|
portBindingsEnabled: {},
|
|
|
|
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.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;
|
|
}
|
|
|
|
$('#appCloneModal').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
|
|
};
|
|
|
|
Client.checkDNSRecords(data.domain, data.location, function (error, result) {
|
|
if (error) {
|
|
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';
|
|
} else {
|
|
$scope.clone.error.location = result.error.message;
|
|
}
|
|
$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;
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
$('#appCloneModal').modal('hide');
|
|
|
|
$location.path('/apps');
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
$scope.repair = {
|
|
retryBusy: false,
|
|
error: {},
|
|
|
|
location: null,
|
|
domain: null,
|
|
alternateDomains: [],
|
|
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.location = null;
|
|
$scope.repair.domain = null;
|
|
$scope.repair.alternateDomains = [];
|
|
$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.location = app.location;
|
|
$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.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]
|
|
};
|
|
});
|
|
}
|
|
|
|
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.location = $scope.repair.location;
|
|
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.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:
|
|
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.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 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 = {
|
|
backupId: backup.id,
|
|
encrypted: !!$scope.backupConfig.password // we add this just to help the import UI
|
|
};
|
|
|
|
Object.keys($scope.backupConfig).forEach(function (k) {
|
|
if ($scope.backupConfig[k] !== SECRET_PLACEHOLDER) tmp[k] = $scope.backupConfig[k];
|
|
});
|
|
|
|
var filename = 'app-backup-config-' + (new Date()).toISOString().replace(/:|T/g,'-').replace(/\..*/,'') + ' (' + $scope.app.fqdn + ')' + '.json';
|
|
download(filename, JSON.stringify(tmp));
|
|
};
|
|
|
|
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') { // patch the backupId to have the full path
|
|
backupConfig.backupId = backupConfig.backupFolder + '/' + backupConfig.backupId;
|
|
delete backupConfig.backupFolder;
|
|
}
|
|
} catch (e) {
|
|
console.error('Unable to parse backup config');
|
|
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' : 'display', 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,
|
|
getDomains,
|
|
getVolumes,
|
|
getBackupConfig
|
|
], function (error) {
|
|
if (error) return Client.error(error);
|
|
|
|
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'].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();
|
|
}]);
|