Files
cloudron-box/dashboard/public/views/app.js

2389 lines
95 KiB
JavaScript
Raw Normal View History

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