mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-07 04:16:16 +02:00
Docker Manager: image refresh, history UI, ports in settings
- container.py: optional force_update on image pull/install; saveContainerSettings accepts normalized client ports on recreate; loadContainerHome exposes ports_json for Angular; _normalize_ports_for_save helper. - manageImages.html: image history modal layout; Refresh control for image pull. - viewContainer.html: port mappings inside Container Settings; ports_json ng-init; checkbox/toggle/compose CSS fixes; remove standalone Edit Ports modal. - dockerManager.js (3x): initAngularPortsFromServer, flat DB port map + inspect fallback, portsDirty/saveSettings integration, advanced-env switch checked styles support, refreshContainerInfo, image history formatting helpers.
This commit is contained in:
@@ -210,6 +210,9 @@ class ContainerManager(multi.Thread):
|
||||
data['memoryUsage'] = 0
|
||||
data['cpuUsage'] = 0
|
||||
|
||||
# JSON for Angular ng-init (flat map container_port -> host_port from DB)
|
||||
data['ports_json'] = json.dumps(data.get('ports', {}))
|
||||
|
||||
template = 'dockerManager/viewContainer.html'
|
||||
proc = httpProc(request, template, data, 'admin')
|
||||
return proc.render()
|
||||
@@ -588,14 +591,21 @@ class ContainerManager(multi.Thread):
|
||||
|
||||
image = data['image']
|
||||
tag = data['tag']
|
||||
force_update = bool(data.get('force_update'))
|
||||
|
||||
try:
|
||||
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
|
||||
data_ret = {'installImageStatus': 0, 'error_message': "Image already installed"}
|
||||
if not self._validate_image_name(image):
|
||||
data_ret = {'installImageStatus': 0, 'error_message': 'Invalid image name format'}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except docker.errors.ImageNotFound:
|
||||
pass
|
||||
|
||||
if not force_update:
|
||||
try:
|
||||
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
|
||||
data_ret = {'installImageStatus': 0, 'error_message': "Image already installed"}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except docker.errors.ImageNotFound:
|
||||
pass
|
||||
|
||||
try:
|
||||
image = client.images.pull(image, tag=tag)
|
||||
@@ -636,14 +646,17 @@ class ContainerManager(multi.Thread):
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
# Check if image already exists
|
||||
try:
|
||||
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
|
||||
data_ret = {'pullImageStatus': 0, 'error_message': "Image already exists locally"}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except docker.errors.ImageNotFound:
|
||||
pass
|
||||
force_update = bool(data.get('force_update'))
|
||||
|
||||
# Check if image already exists (skip when refreshing from registry)
|
||||
if not force_update:
|
||||
try:
|
||||
inspectImage = dockerAPI.inspect_image(image + ":" + tag)
|
||||
data_ret = {'pullImageStatus': 0, 'error_message': "Image already exists locally"}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
except docker.errors.ImageNotFound:
|
||||
pass
|
||||
|
||||
# Pull the image
|
||||
try:
|
||||
@@ -1306,6 +1319,36 @@ class ContainerManager(multi.Thread):
|
||||
secure_log_error(e, 'container_operation')
|
||||
return 'Operation failed'
|
||||
|
||||
@staticmethod
|
||||
def _normalize_ports_for_save(ports_raw):
|
||||
"""
|
||||
Parse client-submitted port map for saveContainerSettings recreate.
|
||||
Returns (True, dict) with container_port -> host_port (str),
|
||||
(True, None) if ports_raw is None (caller should use DB),
|
||||
or (False, error_message).
|
||||
"""
|
||||
if ports_raw is None:
|
||||
return True, None
|
||||
if not isinstance(ports_raw, dict):
|
||||
return False, 'Invalid ports payload'
|
||||
out = {}
|
||||
for ck, cv in ports_raw.items():
|
||||
if not ck:
|
||||
continue
|
||||
ckey = str(ck).strip()
|
||||
if not ckey:
|
||||
continue
|
||||
if cv in (None, '', 'null'):
|
||||
continue
|
||||
try:
|
||||
hp = int(cv)
|
||||
except (ValueError, TypeError):
|
||||
return False, 'Invalid host port for %s' % ckey
|
||||
if hp < 1024 or hp > 65535:
|
||||
return False, 'Choose host port between 1024 and 65535'
|
||||
out[ckey] = str(hp)
|
||||
return True, out
|
||||
|
||||
def saveContainerSettings(self, userID=None, data=None):
|
||||
try:
|
||||
name = data['name']
|
||||
@@ -1375,6 +1418,15 @@ class ContainerManager(multi.Thread):
|
||||
continue
|
||||
volumes[volume['src']] = {'bind': volume['dest'],
|
||||
'mode': 'rw'}
|
||||
ports_for_recreate = json.loads(con.ports)
|
||||
if 'ports' in data and data.get('ports') is not None:
|
||||
ok_ports, norm_ports = self._normalize_ports_for_save(data.get('ports'))
|
||||
if not ok_ports:
|
||||
data_ret = {'saveSettingsStatus': 0, 'error_message': norm_ports}
|
||||
json_data = json.dumps(data_ret)
|
||||
return HttpResponse(json_data)
|
||||
if norm_ports is not None:
|
||||
ports_for_recreate = norm_ports
|
||||
# Prepare data for recreate function
|
||||
data = {
|
||||
'name': name,
|
||||
@@ -1382,7 +1434,7 @@ class ContainerManager(multi.Thread):
|
||||
'image': con.image,
|
||||
'tag': con.tag,
|
||||
'env': envDict,
|
||||
'ports': json.loads(con.ports),
|
||||
'ports': ports_for_recreate,
|
||||
'volumes': volumes,
|
||||
'memory': con.memory
|
||||
}
|
||||
@@ -1395,6 +1447,7 @@ class ContainerManager(multi.Thread):
|
||||
|
||||
con.env = json.dumps(envDict)
|
||||
con.volumes = json.dumps(volumes)
|
||||
con.ports = json.dumps(ports_for_recreate)
|
||||
con.save()
|
||||
|
||||
data_ret = {'saveSettingsStatus': 1, 'error_message': 'None'}
|
||||
|
||||
@@ -1389,6 +1389,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.cName = "";
|
||||
$scope.status = "";
|
||||
$scope.savingSettings = false;
|
||||
$scope.currentPorts = {};
|
||||
$scope.initialPortsForSettings = {};
|
||||
$scope.loadingTop = false;
|
||||
$scope.statusInterval = null;
|
||||
$scope.statsInterval = null;
|
||||
@@ -1398,7 +1400,37 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.advancedEnvText = '';
|
||||
$scope.advancedEnvCount = 0;
|
||||
$scope.parsedEnvVars = {};
|
||||
|
||||
/** Flat map from DB (container_port -> host_port), set from template ng-init */
|
||||
$scope.serverPortMap = {};
|
||||
|
||||
$scope.initAngularPortsFromServer = function (obj) {
|
||||
if (obj === undefined || obj === null) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
if (angular.isString(obj)) {
|
||||
try {
|
||||
obj = angular.fromJson(obj);
|
||||
} catch (e1) {
|
||||
try {
|
||||
obj = JSON.parse(obj);
|
||||
} catch (e2) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!angular.isObject(obj)) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
$scope.serverPortMap = obj;
|
||||
$scope.ports = obj;
|
||||
};
|
||||
|
||||
// Auto-refresh status every 5 seconds
|
||||
$scope.startStatusMonitoring = function() {
|
||||
$scope.statusInterval = $interval(function() {
|
||||
@@ -2028,6 +2060,13 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Used after legacy port-update path; full page reload syncs Django-rendered state */
|
||||
$scope.refreshContainerInfo = function () {
|
||||
$timeout(function () {
|
||||
window.location.reload();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
$scope.addVolField = function () {
|
||||
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
|
||||
$scope.volListNumber = $scope.volListNumber + 1;
|
||||
@@ -2038,6 +2077,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
if ($scope.portsDirty && $scope.portsDirty() && !$scope.envConfirmation) {
|
||||
new PNotify({
|
||||
title: 'Confirmation required',
|
||||
text: 'Check the confirmation box to apply changes to ports, environment variables, or volumes (the container will be recreated).',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$('#containerSettingLoading').show();
|
||||
url = "/docker/saveContainerSettings";
|
||||
$scope.savingSettings = true;
|
||||
@@ -2063,7 +2111,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
envConfirmation: $scope.envConfirmation,
|
||||
envList: finalEnvList,
|
||||
volList: $scope.volList,
|
||||
advancedEnvMode: $scope.advancedEnvMode
|
||||
advancedEnvMode: $scope.advancedEnvMode,
|
||||
ports: $scope.currentPorts || {}
|
||||
};
|
||||
|
||||
|
||||
@@ -2088,6 +2137,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
title: 'Settings Saved',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts || {});
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -2213,22 +2263,55 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$("#commandModal").modal("show");
|
||||
};
|
||||
|
||||
// Port editing functionality
|
||||
$scope.showPortEditModal = function() {
|
||||
// Initialize current ports from container data
|
||||
// Port editing (in Container Settings modal)
|
||||
$scope.initSettingsPortsFromContainer = function () {
|
||||
$scope.currentPorts = {};
|
||||
if ($scope.ports) {
|
||||
for (var iport in $scope.ports) {
|
||||
var eport = $scope.ports[iport];
|
||||
if (eport && eport.length > 0) {
|
||||
var src = $scope.serverPortMap && Object.keys($scope.serverPortMap).length
|
||||
? $scope.serverPortMap
|
||||
: ($scope.ports || {});
|
||||
if (src && typeof src === 'object') {
|
||||
for (var iport in src) {
|
||||
if (!Object.prototype.hasOwnProperty.call(src, iport)) {
|
||||
continue;
|
||||
}
|
||||
var eport = src[iport];
|
||||
if (angular.isArray(eport) && eport.length > 0 && eport[0] && eport[0].HostPort) {
|
||||
$scope.currentPorts[iport] = eport[0].HostPort;
|
||||
} else if (eport !== undefined && eport !== null && eport !== '') {
|
||||
$scope.currentPorts[iport] = String(eport);
|
||||
}
|
||||
}
|
||||
}
|
||||
$("#portEditModal").modal("show");
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts);
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function() {
|
||||
$scope.portsJsonSnapshot = function (obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return '{}';
|
||||
}
|
||||
var keys = Object.keys(obj).sort();
|
||||
var flat = {};
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
var v = obj[k];
|
||||
flat[k] = v === null || v === undefined ? '' : String(v);
|
||||
}
|
||||
return JSON.stringify(flat);
|
||||
};
|
||||
|
||||
$scope.portsDirty = function () {
|
||||
return $scope.portsJsonSnapshot($scope.currentPorts) !== $scope.portsJsonSnapshot($scope.initialPortsForSettings);
|
||||
};
|
||||
|
||||
$scope.showPortEditModal = function () {
|
||||
$scope.initSettingsPortsFromContainer();
|
||||
$("#settings").modal("show");
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function () {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
|
||||
if (containerPort) {
|
||||
$scope.currentPorts[containerPort] = '';
|
||||
@@ -2236,14 +2319,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removePortMapping = function(containerPort) {
|
||||
$scope.removePortMapping = function (containerPort) {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
if (confirm('Are you sure you want to remove this port mapping?')) {
|
||||
delete $scope.currentPorts[containerPort];
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updatePortMappings = function() {
|
||||
$("#portEditLoading").show();
|
||||
$scope.updatePortMappings = function () {
|
||||
$scope.updatingPorts = true;
|
||||
|
||||
var url = "/docker/updateContainerPorts";
|
||||
@@ -2259,18 +2344,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
$("#portEditLoading").hide();
|
||||
$scope.updatingPorts = false;
|
||||
|
||||
if (response.data.status === 1) {
|
||||
$("#portEditModal").modal("hide");
|
||||
// Refresh container status and ports
|
||||
$scope.refreshContainerInfo();
|
||||
$("#settings").modal("hide");
|
||||
new PNotify({
|
||||
title: 'Success',
|
||||
text: 'Port mappings updated successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.refreshContainerInfo();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
@@ -2279,11 +2362,10 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
$("#portEditLoading").hide();
|
||||
$scope.updatingPorts = false;
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: 'Error updating port mappings: ' + error.data.error_message,
|
||||
text: 'Error updating port mappings: ' + (error.data && error.data.error_message ? error.data.error_message : 'unknown'),
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
@@ -2388,12 +2470,55 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
$scope.showingSearch = false;
|
||||
$("#searchResult").hide();
|
||||
|
||||
function dockerHistoryFormatSize(raw) {
|
||||
var n = parseInt(raw, 10);
|
||||
if (isNaN(n) || n < 0) {
|
||||
return (raw === undefined || raw === null) ? '' : String(raw);
|
||||
}
|
||||
if (n === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
|
||||
var v = n / Math.pow(1024, i);
|
||||
var s = (i === 0) ? String(v) : (Math.round(v * 10) / 10).toFixed(1);
|
||||
return s + ' ' + units[i];
|
||||
}
|
||||
|
||||
function dockerHistoryFormatCreated(raw) {
|
||||
if (raw === undefined || raw === null) {
|
||||
return '';
|
||||
}
|
||||
var n = Number(raw);
|
||||
if (isNaN(n)) {
|
||||
return String(raw);
|
||||
}
|
||||
var sec = n;
|
||||
if (n > 1e14) {
|
||||
sec = Math.floor(n / 1e9);
|
||||
} else if (n > 1e12) {
|
||||
sec = Math.floor(n / 1e6);
|
||||
}
|
||||
var d = new Date(sec * 1000);
|
||||
if (isNaN(d.getTime())) {
|
||||
return String(raw);
|
||||
}
|
||||
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
||||
}
|
||||
|
||||
function dockerHistoryEnrichLayer(h) {
|
||||
var o = angular.extend({}, h);
|
||||
o.SizeHuman = dockerHistoryFormatSize(h.Size);
|
||||
o.CreatedDisplay = dockerHistoryFormatCreated(h.Created);
|
||||
return o;
|
||||
}
|
||||
|
||||
$scope.pullImage = function (image, tag) {
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.installImageStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Image pulled successfully',
|
||||
text: 'Reloading...',
|
||||
text: 'Running containers keep the old image until you recreate them from Docker > Containers. Reloading…',
|
||||
type: 'success'
|
||||
});
|
||||
location.reload()
|
||||
@@ -2424,7 +2549,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
url = "/docker/installImage";
|
||||
var data = {
|
||||
image: image,
|
||||
tag: tag
|
||||
tag: tag,
|
||||
force_update: true
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
@@ -2445,6 +2571,30 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
}
|
||||
|
||||
$scope.refreshLocalImage = function (rowId, imageName) {
|
||||
var sel = document.getElementById(String(rowId));
|
||||
if (!sel || sel.selectedIndex < 0) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var raw = sel.options[sel.selectedIndex].text;
|
||||
if (!raw) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var li = raw.lastIndexOf(':');
|
||||
var tag = li > -1 ? raw.substring(li + 1) : raw;
|
||||
$scope.pullImage(imageName, tag);
|
||||
}
|
||||
|
||||
$scope.searchImages = function () {
|
||||
console.log($scope.searchString);
|
||||
if (!$scope.searchString) {
|
||||
@@ -2580,7 +2730,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
$scope.getHistory = function (counter) {
|
||||
$('#imageLoading').show();
|
||||
var name = $("#" + counter).val()
|
||||
var sel = $("#" + counter);
|
||||
var name = (sel.find('option:selected').text() || sel.val() || '').trim();
|
||||
|
||||
url = "/docker/getImageHistory";
|
||||
|
||||
@@ -2600,7 +2751,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
if (response.data.imageHistoryStatus === 1) {
|
||||
$('#history').modal('show');
|
||||
$scope.historyList = response.data.history;
|
||||
var raw = response.data.history || [];
|
||||
$scope.historyList = raw.map(dockerHistoryEnrichLayer);
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
|
||||
@@ -375,8 +375,20 @@
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
.modal-dialog-history {
|
||||
width: 96vw;
|
||||
max-width: min(96vw, 1400px);
|
||||
margin: 1.75rem auto;
|
||||
}
|
||||
|
||||
.history-modal-scroll {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.history-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
background: var(--bg-hover, #f8f9ff);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
@@ -390,13 +402,74 @@
|
||||
color: var(--text-primary, #1e293b);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.history-table th.history-col-size {
|
||||
text-align: right;
|
||||
width: 9%;
|
||||
}
|
||||
|
||||
.history-table th.history-col-created {
|
||||
width: 14%;
|
||||
}
|
||||
|
||||
.history-table th.history-col-comment {
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
.history-table th.history-col-id {
|
||||
width: 14%;
|
||||
}
|
||||
|
||||
.history-table td {
|
||||
padding: 0.75rem;
|
||||
color: var(--text-secondary, #64748b);
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 1px solid var(--border-color, #e8e9ff);
|
||||
word-break: break-all;
|
||||
vertical-align: top;
|
||||
word-break: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.history-table td.history-col-id {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
max-width: 0;
|
||||
}
|
||||
|
||||
.history-table td.history-col-cmd {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.history-layer-cmd {
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
max-height: 14rem;
|
||||
max-width: 100%;
|
||||
background: var(--bg-primary, #fff);
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.6rem;
|
||||
}
|
||||
|
||||
.history-table td.history-col-created {
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.history-table td.history-col-comment {
|
||||
overflow-x: auto;
|
||||
max-width: 0;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.history-table td.history-col-size {
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
@@ -583,6 +656,11 @@
|
||||
</td>
|
||||
<td style="text-align: center;">
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="btn-primary" title="{% trans 'Pull latest from registry for the selected tag. Running containers are unchanged until recreated.' %}"
|
||||
ng-click="refreshLocalImage({{ forloop.counter }}, '{{ image.name|escapejs }}')">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
{% trans "Refresh" %}
|
||||
</button>
|
||||
<button class="btn-info" title="{% trans 'View History' %}"
|
||||
ng-click="getHistory({{ forloop.counter }})">
|
||||
<i class="fas fa-history"></i>
|
||||
@@ -603,7 +681,7 @@
|
||||
|
||||
<!-- History Modal -->
|
||||
<div id="history" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-dialog modal-lg modal-dialog-history">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
@@ -614,26 +692,28 @@
|
||||
style="font-size: 1.5rem; background: transparent; border: none;">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="history-modal-scroll">
|
||||
<table class="history-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th class="history-col-id">{% trans "ID" %}</th>
|
||||
<th>{% trans "Created By" %}</th>
|
||||
<th>{% trans "Created" %}</th>
|
||||
<th>{% trans "Comment" %}</th>
|
||||
<th>{% trans "Size" %}</th>
|
||||
<th class="history-col-created">{% trans "Created" %}</th>
|
||||
<th class="history-col-comment">{% trans "Comment" %}</th>
|
||||
<th class="history-col-size">{% trans "Size" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="history in historyList track by $index">
|
||||
<td ng-bind="history.Id"></td>
|
||||
<td ng-bind="history.CreatedBy"></td>
|
||||
<td ng-bind="history.Created"></td>
|
||||
<td ng-bind="history.Comment"></td>
|
||||
<td ng-bind="history.Size"></td>
|
||||
<td class="history-col-id" ng-bind="history.Id"></td>
|
||||
<td class="history-col-cmd"><pre class="history-layer-cmd" ng-bind="history.CreatedBy"></pre></td>
|
||||
<td class="history-col-created" ng-bind="history.CreatedDisplay"></td>
|
||||
<td class="history-col-comment" ng-bind="history.Comment"></td>
|
||||
<td class="history-col-size"><span ng-bind="history.SizeHuman"></span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" data-dismiss="modal">
|
||||
|
||||
@@ -589,7 +589,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="modern-container" ng-controller="viewContainer">
|
||||
<div class="modern-container" ng-controller="viewContainer" ng-init='initAngularPortsFromServer({{ ports_json|safe }})'>
|
||||
<div class="page-header">
|
||||
<div class="container-header">
|
||||
<div class="container-icon-large">
|
||||
@@ -738,7 +738,7 @@
|
||||
<div class="action-text">{% trans "Remove" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="action-btn" data-toggle="modal" data-target="#settings">
|
||||
<div class="action-btn" data-toggle="modal" data-target="#settings" ng-click="initSettingsPortsFromContainer()">
|
||||
<i class="fas fa-sliders-h action-icon" style="color: #6366f1;"></i>
|
||||
<div class="action-text">{% trans "Settings" %}</div>
|
||||
</div>
|
||||
@@ -762,11 +762,6 @@
|
||||
<i class="fas fa-code action-icon" style="color: #10b981;"></i>
|
||||
<div class="action-text">{% trans "Run Command" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="action-btn" ng-click="showPortEditModal()" ng-disabled="status!='running'">
|
||||
<i class="fas fa-edit action-icon" style="color: #8b5cf6;"></i>
|
||||
<div class="action-text">{% trans "Edit Ports" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -826,6 +821,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">{% trans "Port mappings" %}</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>{% trans "Important:" %}</strong>
|
||||
{% trans "Changing host ports requires checking the confirmation box below; the container will be recreated." %}
|
||||
</div>
|
||||
<div class="port-mapping-container">
|
||||
<div ng-repeat="(containerPort, hostPort) in currentPorts" class="port-mapping-row" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
|
||||
<div style="flex: 1;">
|
||||
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Container Port" %}</label>
|
||||
<input type="text" class="form-control" ng-model="containerPort" disabled style="font-family: monospace;">
|
||||
</div>
|
||||
<div style="color: #007bff; font-size: 1.25rem;">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Host Port" %}</label>
|
||||
<input type="number" class="form-control" ng-model="currentPorts[containerPort]"
|
||||
placeholder="{% trans 'e.g., 8080' %}" min="1024" max="65535" ng-disabled="!envConfirmation">
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; height: 38px;">
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="removePortMapping(containerPort)"
|
||||
ng-show="Object.keys(currentPorts).length > 0" title="{% trans 'Remove port mapping' %}" ng-disabled="!envConfirmation">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="Object.keys(currentPorts).length === 0" class="text-center text-muted" style="padding: 2rem;">
|
||||
<i class="fas fa-info-circle fa-2x" style="margin-bottom: 1rem;"></i>
|
||||
<p>{% trans "No port mappings configured" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 0.5rem;">
|
||||
<button type="button" class="btn btn-primary" ng-click="addNewPortMapping()" ng-disabled="!envConfirmation">
|
||||
<i class="fas fa-plus"></i>
|
||||
{% trans "Add Port Mapping" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -834,7 +874,7 @@
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input ng-model="envConfirmation" type="checkbox">
|
||||
<strong>{% trans "I understand that editing ENV or Volumes will recreate the container" %}</strong>
|
||||
<strong>{% trans "I understand that editing ENV, Volumes, or Ports will recreate the container" %}</strong>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -856,10 +896,10 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="toggle-switch">
|
||||
<label class="switch" style="position: relative; display: inline-block; width: 60px; height: 34px;">
|
||||
<input type="checkbox" ng-model="advancedEnvMode" ng-change="toggleEnvMode()" style="opacity: 0; width: 0; height: 0;">
|
||||
<span class="slider" style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px;">
|
||||
<span class="slider-thumb" style="position: absolute; content: ''; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; box-shadow: 0 2px 4px rgba(0,0,0,0.2);"></span>
|
||||
<label class="switch cp-adv-env-switch" style="position: relative; display: inline-block; width: 60px; height: 34px;">
|
||||
<input type="checkbox" ng-model="advancedEnvMode" ng-change="toggleEnvMode()">
|
||||
<span class="slider cp-adv-env-slider">
|
||||
<span class="slider-thumb cp-adv-env-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -1246,77 +1286,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Port Editing Modal -->
|
||||
<div id="portEditModal" class="modal fade" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<i class="fas fa-edit" style="margin-right: 0.5rem;"></i>
|
||||
{% trans "Edit Port Mappings" %}
|
||||
</h4>
|
||||
<button type="button" class="close" data-dismiss="modal"
|
||||
style="font-size: 1.5rem; background: transparent; border: none;">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<strong>{% trans "Important:" %}</strong> {% trans "Editing port mappings will temporarily stop and recreate the container. Any unsaved data in the container may be lost." %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">
|
||||
<i class="fas fa-plug" style="margin-right: 0.5rem;"></i>
|
||||
{% trans "Port Mappings" %}
|
||||
</label>
|
||||
<div class="port-mapping-container">
|
||||
<div ng-repeat="(containerPort, hostPort) in currentPorts" class="port-mapping-row" style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 8px;">
|
||||
<div style="flex: 1;">
|
||||
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Container Port" %}</label>
|
||||
<input type="text" class="form-control" ng-model="containerPort" disabled style="font-family: monospace;">
|
||||
</div>
|
||||
<div style="color: #007bff; font-size: 1.25rem;">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<label style="font-weight: 600; color: #495057; margin-bottom: 0.25rem;">{% trans "Host Port" %}</label>
|
||||
<input type="number" class="form-control" ng-model="currentPorts[containerPort]"
|
||||
placeholder="{% trans 'e.g., 8080' %}" min="1024" max="65535">
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; height: 38px;">
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="removePortMapping(containerPort)"
|
||||
ng-show="Object.keys(currentPorts).length > 1" title="{% trans 'Remove port mapping' %}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="Object.keys(currentPorts).length === 0" class="text-center text-muted" style="padding: 2rem;">
|
||||
<i class="fas fa-info-circle fa-2x" style="margin-bottom: 1rem;"></i>
|
||||
<p>{% trans "No port mappings configured" %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="button" class="btn btn-primary" ng-click="addNewPortMapping()">
|
||||
<i class="fas fa-plus"></i>
|
||||
{% trans "Add Port Mapping" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<img id="portEditLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
|
||||
<button type="button" class="btn btn-primary" ng-disabled="updatingPorts" ng-click="updatePortMappings()">
|
||||
<i class="fas fa-save"></i> {% trans "Update Port Mappings" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
<i class="fas fa-times"></i> {% trans "Cancel" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -1439,13 +1408,79 @@
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.compose-benefits {
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.compose-actions {
|
||||
margin-top: 1rem;
|
||||
margin-top: 1.25rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid rgba(0, 123, 255, 0.15);
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Container Settings modal: checkbox alignment + advanced mode switch */
|
||||
#settings .checkbox label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.65rem;
|
||||
padding-left: 0;
|
||||
cursor: pointer;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#settings .checkbox label input[type="checkbox"] {
|
||||
position: static !important;
|
||||
margin-left: 0 !important;
|
||||
margin-top: 0.2rem;
|
||||
flex-shrink: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-thumb {
|
||||
position: absolute;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: #fff;
|
||||
transition: 0.3s;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-switch input:checked + .cp-adv-env-slider {
|
||||
background-color: #6366f1;
|
||||
}
|
||||
|
||||
#settings .cp-adv-env-switch input:checked + .cp-adv-env-slider .cp-adv-env-thumb {
|
||||
left: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.compose-actions .btn {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
|
||||
@@ -974,6 +974,15 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.dockerUpdateInProgress) {
|
||||
new PNotify({
|
||||
title: 'Update in progress',
|
||||
text: 'Wait until the current update finishes before starting another.',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no new image specified, use current image
|
||||
if (!$scope.newImage) {
|
||||
$scope.newImage = $scope.currentImage;
|
||||
@@ -1000,9 +1009,18 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
history: false
|
||||
}
|
||||
})).get().on('pnotify.confirm', function () {
|
||||
var dockerUpdateNotificationId = null;
|
||||
$scope.dockerUpdateInProgress = true;
|
||||
$('#imageLoading').show();
|
||||
$("#updateContainer").modal("hide");
|
||||
|
||||
if (typeof window.cpDockerUpdateNotifyStart === 'function') {
|
||||
dockerUpdateNotificationId = window.cpDockerUpdateNotifyStart(
|
||||
$scope.updateContainerName,
|
||||
$scope.newImage + ':' + $scope.newTag
|
||||
);
|
||||
}
|
||||
|
||||
url = "/docker/updateContainer";
|
||||
var data = {
|
||||
name: $scope.updateContainerName,
|
||||
@@ -1020,15 +1038,27 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
|
||||
function ListInitialData(response) {
|
||||
console.log(response);
|
||||
$scope.dockerUpdateInProgress = false;
|
||||
$('#imageLoading').hide();
|
||||
|
||||
if (response.data.updateContainerStatus === 1) {
|
||||
var ok = response.data && response.data.updateContainerStatus === 1;
|
||||
var imgLabel = ok
|
||||
? (response.data.new_image || response.data.message || 'Updated')
|
||||
: (response.data && response.data.error_message ? response.data.error_message : 'Update failed');
|
||||
|
||||
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
|
||||
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, ok, imgLabel);
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
new PNotify({
|
||||
title: 'Container Updated Successfully',
|
||||
text: `Container updated to ${response.data.new_image}`,
|
||||
text: 'Container updated to ' + (response.data.new_image || response.data.message || 'new image'),
|
||||
type: 'success'
|
||||
});
|
||||
location.reload();
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 2200);
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
@@ -1039,7 +1069,11 @@ app.controller('listContainers', function ($scope, $http) {
|
||||
}
|
||||
|
||||
function cantLoadInitialData(response) {
|
||||
$scope.dockerUpdateInProgress = false;
|
||||
$('#imageLoading').hide();
|
||||
if (typeof window.cpDockerUpdateNotifyEnd === 'function' && dockerUpdateNotificationId) {
|
||||
window.cpDockerUpdateNotifyEnd(dockerUpdateNotificationId, false, 'Could not connect to server');
|
||||
}
|
||||
new PNotify({
|
||||
title: 'Update Failed',
|
||||
text: 'Could not connect to server',
|
||||
@@ -1355,6 +1389,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.cName = "";
|
||||
$scope.status = "";
|
||||
$scope.savingSettings = false;
|
||||
$scope.currentPorts = {};
|
||||
$scope.initialPortsForSettings = {};
|
||||
$scope.loadingTop = false;
|
||||
$scope.statusInterval = null;
|
||||
$scope.statsInterval = null;
|
||||
@@ -1364,7 +1400,37 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.advancedEnvText = '';
|
||||
$scope.advancedEnvCount = 0;
|
||||
$scope.parsedEnvVars = {};
|
||||
|
||||
/** Flat map from DB (container_port -> host_port), set from template ng-init */
|
||||
$scope.serverPortMap = {};
|
||||
|
||||
$scope.initAngularPortsFromServer = function (obj) {
|
||||
if (obj === undefined || obj === null) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
if (angular.isString(obj)) {
|
||||
try {
|
||||
obj = angular.fromJson(obj);
|
||||
} catch (e1) {
|
||||
try {
|
||||
obj = JSON.parse(obj);
|
||||
} catch (e2) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!angular.isObject(obj)) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
$scope.serverPortMap = obj;
|
||||
$scope.ports = obj;
|
||||
};
|
||||
|
||||
// Auto-refresh status every 5 seconds
|
||||
$scope.startStatusMonitoring = function() {
|
||||
$scope.statusInterval = $interval(function() {
|
||||
@@ -1994,6 +2060,13 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Used after legacy port-update path; full page reload syncs Django-rendered state */
|
||||
$scope.refreshContainerInfo = function () {
|
||||
$timeout(function () {
|
||||
window.location.reload();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
$scope.addVolField = function () {
|
||||
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
|
||||
$scope.volListNumber = $scope.volListNumber + 1;
|
||||
@@ -2004,6 +2077,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
if ($scope.portsDirty && $scope.portsDirty() && !$scope.envConfirmation) {
|
||||
new PNotify({
|
||||
title: 'Confirmation required',
|
||||
text: 'Check the confirmation box to apply changes to ports, environment variables, or volumes (the container will be recreated).',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$('#containerSettingLoading').show();
|
||||
url = "/docker/saveContainerSettings";
|
||||
$scope.savingSettings = true;
|
||||
@@ -2029,7 +2111,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
envConfirmation: $scope.envConfirmation,
|
||||
envList: finalEnvList,
|
||||
volList: $scope.volList,
|
||||
advancedEnvMode: $scope.advancedEnvMode
|
||||
advancedEnvMode: $scope.advancedEnvMode,
|
||||
ports: $scope.currentPorts || {}
|
||||
};
|
||||
|
||||
|
||||
@@ -2054,6 +2137,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
title: 'Settings Saved',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts || {});
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -2179,22 +2263,55 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$("#commandModal").modal("show");
|
||||
};
|
||||
|
||||
// Port editing functionality
|
||||
$scope.showPortEditModal = function() {
|
||||
// Initialize current ports from container data
|
||||
// Port editing (in Container Settings modal)
|
||||
$scope.initSettingsPortsFromContainer = function () {
|
||||
$scope.currentPorts = {};
|
||||
if ($scope.ports) {
|
||||
for (var iport in $scope.ports) {
|
||||
var eport = $scope.ports[iport];
|
||||
if (eport && eport.length > 0) {
|
||||
var src = $scope.serverPortMap && Object.keys($scope.serverPortMap).length
|
||||
? $scope.serverPortMap
|
||||
: ($scope.ports || {});
|
||||
if (src && typeof src === 'object') {
|
||||
for (var iport in src) {
|
||||
if (!Object.prototype.hasOwnProperty.call(src, iport)) {
|
||||
continue;
|
||||
}
|
||||
var eport = src[iport];
|
||||
if (angular.isArray(eport) && eport.length > 0 && eport[0] && eport[0].HostPort) {
|
||||
$scope.currentPorts[iport] = eport[0].HostPort;
|
||||
} else if (eport !== undefined && eport !== null && eport !== '') {
|
||||
$scope.currentPorts[iport] = String(eport);
|
||||
}
|
||||
}
|
||||
}
|
||||
$("#portEditModal").modal("show");
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts);
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function() {
|
||||
$scope.portsJsonSnapshot = function (obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return '{}';
|
||||
}
|
||||
var keys = Object.keys(obj).sort();
|
||||
var flat = {};
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
var v = obj[k];
|
||||
flat[k] = v === null || v === undefined ? '' : String(v);
|
||||
}
|
||||
return JSON.stringify(flat);
|
||||
};
|
||||
|
||||
$scope.portsDirty = function () {
|
||||
return $scope.portsJsonSnapshot($scope.currentPorts) !== $scope.portsJsonSnapshot($scope.initialPortsForSettings);
|
||||
};
|
||||
|
||||
$scope.showPortEditModal = function () {
|
||||
$scope.initSettingsPortsFromContainer();
|
||||
$("#settings").modal("show");
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function () {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
|
||||
if (containerPort) {
|
||||
$scope.currentPorts[containerPort] = '';
|
||||
@@ -2202,14 +2319,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removePortMapping = function(containerPort) {
|
||||
$scope.removePortMapping = function (containerPort) {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
if (confirm('Are you sure you want to remove this port mapping?')) {
|
||||
delete $scope.currentPorts[containerPort];
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updatePortMappings = function() {
|
||||
$("#portEditLoading").show();
|
||||
$scope.updatePortMappings = function () {
|
||||
$scope.updatingPorts = true;
|
||||
|
||||
var url = "/docker/updateContainerPorts";
|
||||
@@ -2225,18 +2344,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
$("#portEditLoading").hide();
|
||||
$scope.updatingPorts = false;
|
||||
|
||||
if (response.data.status === 1) {
|
||||
$("#portEditModal").modal("hide");
|
||||
// Refresh container status and ports
|
||||
$scope.refreshContainerInfo();
|
||||
$("#settings").modal("hide");
|
||||
new PNotify({
|
||||
title: 'Success',
|
||||
text: 'Port mappings updated successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.refreshContainerInfo();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
@@ -2245,11 +2362,10 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
$("#portEditLoading").hide();
|
||||
$scope.updatingPorts = false;
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: 'Error updating port mappings: ' + error.data.error_message,
|
||||
text: 'Error updating port mappings: ' + (error.data && error.data.error_message ? error.data.error_message : 'unknown'),
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
@@ -2354,12 +2470,55 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
$scope.showingSearch = false;
|
||||
$("#searchResult").hide();
|
||||
|
||||
function dockerHistoryFormatSize(raw) {
|
||||
var n = parseInt(raw, 10);
|
||||
if (isNaN(n) || n < 0) {
|
||||
return (raw === undefined || raw === null) ? '' : String(raw);
|
||||
}
|
||||
if (n === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
|
||||
var v = n / Math.pow(1024, i);
|
||||
var s = (i === 0) ? String(v) : (Math.round(v * 10) / 10).toFixed(1);
|
||||
return s + ' ' + units[i];
|
||||
}
|
||||
|
||||
function dockerHistoryFormatCreated(raw) {
|
||||
if (raw === undefined || raw === null) {
|
||||
return '';
|
||||
}
|
||||
var n = Number(raw);
|
||||
if (isNaN(n)) {
|
||||
return String(raw);
|
||||
}
|
||||
var sec = n;
|
||||
if (n > 1e14) {
|
||||
sec = Math.floor(n / 1e9);
|
||||
} else if (n > 1e12) {
|
||||
sec = Math.floor(n / 1e6);
|
||||
}
|
||||
var d = new Date(sec * 1000);
|
||||
if (isNaN(d.getTime())) {
|
||||
return String(raw);
|
||||
}
|
||||
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
||||
}
|
||||
|
||||
function dockerHistoryEnrichLayer(h) {
|
||||
var o = angular.extend({}, h);
|
||||
o.SizeHuman = dockerHistoryFormatSize(h.Size);
|
||||
o.CreatedDisplay = dockerHistoryFormatCreated(h.Created);
|
||||
return o;
|
||||
}
|
||||
|
||||
$scope.pullImage = function (image, tag) {
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.installImageStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Image pulled successfully',
|
||||
text: 'Reloading...',
|
||||
text: 'Running containers keep the old image until you recreate them from Docker > Containers. Reloading…',
|
||||
type: 'success'
|
||||
});
|
||||
location.reload()
|
||||
@@ -2390,7 +2549,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
url = "/docker/installImage";
|
||||
var data = {
|
||||
image: image,
|
||||
tag: tag
|
||||
tag: tag,
|
||||
force_update: true
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
@@ -2411,6 +2571,30 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
}
|
||||
|
||||
$scope.refreshLocalImage = function (rowId, imageName) {
|
||||
var sel = document.getElementById(String(rowId));
|
||||
if (!sel || sel.selectedIndex < 0) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var raw = sel.options[sel.selectedIndex].text;
|
||||
if (!raw) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var li = raw.lastIndexOf(':');
|
||||
var tag = li > -1 ? raw.substring(li + 1) : raw;
|
||||
$scope.pullImage(imageName, tag);
|
||||
}
|
||||
|
||||
$scope.searchImages = function () {
|
||||
console.log($scope.searchString);
|
||||
if (!$scope.searchString) {
|
||||
@@ -2546,7 +2730,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
$scope.getHistory = function (counter) {
|
||||
$('#imageLoading').show();
|
||||
var name = $("#" + counter).val()
|
||||
var sel = $("#" + counter);
|
||||
var name = (sel.find('option:selected').text() || sel.val() || '').trim();
|
||||
|
||||
url = "/docker/getImageHistory";
|
||||
|
||||
@@ -2566,7 +2751,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
if (response.data.imageHistoryStatus === 1) {
|
||||
$('#history').modal('show');
|
||||
$scope.historyList = response.data.history;
|
||||
var raw = response.data.history || [];
|
||||
$scope.historyList = raw.map(dockerHistoryEnrichLayer);
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
@@ -2713,4 +2899,3 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1389,6 +1389,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.cName = "";
|
||||
$scope.status = "";
|
||||
$scope.savingSettings = false;
|
||||
$scope.currentPorts = {};
|
||||
$scope.initialPortsForSettings = {};
|
||||
$scope.loadingTop = false;
|
||||
$scope.statusInterval = null;
|
||||
$scope.statsInterval = null;
|
||||
@@ -1398,7 +1400,37 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$scope.advancedEnvText = '';
|
||||
$scope.advancedEnvCount = 0;
|
||||
$scope.parsedEnvVars = {};
|
||||
|
||||
/** Flat map from DB (container_port -> host_port), set from template ng-init */
|
||||
$scope.serverPortMap = {};
|
||||
|
||||
$scope.initAngularPortsFromServer = function (obj) {
|
||||
if (obj === undefined || obj === null) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
if (angular.isString(obj)) {
|
||||
try {
|
||||
obj = angular.fromJson(obj);
|
||||
} catch (e1) {
|
||||
try {
|
||||
obj = JSON.parse(obj);
|
||||
} catch (e2) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!angular.isObject(obj)) {
|
||||
$scope.serverPortMap = {};
|
||||
$scope.ports = {};
|
||||
return;
|
||||
}
|
||||
$scope.serverPortMap = obj;
|
||||
$scope.ports = obj;
|
||||
};
|
||||
|
||||
// Auto-refresh status every 5 seconds
|
||||
$scope.startStatusMonitoring = function() {
|
||||
$scope.statusInterval = $interval(function() {
|
||||
@@ -2028,6 +2060,13 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
/** Used after legacy port-update path; full page reload syncs Django-rendered state */
|
||||
$scope.refreshContainerInfo = function () {
|
||||
$timeout(function () {
|
||||
window.location.reload();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
$scope.addVolField = function () {
|
||||
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
|
||||
$scope.volListNumber = $scope.volListNumber + 1;
|
||||
@@ -2038,6 +2077,15 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
if ($scope.portsDirty && $scope.portsDirty() && !$scope.envConfirmation) {
|
||||
new PNotify({
|
||||
title: 'Confirmation required',
|
||||
text: 'Check the confirmation box to apply changes to ports, environment variables, or volumes (the container will be recreated).',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$('#containerSettingLoading').show();
|
||||
url = "/docker/saveContainerSettings";
|
||||
$scope.savingSettings = true;
|
||||
@@ -2063,7 +2111,8 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
envConfirmation: $scope.envConfirmation,
|
||||
envList: finalEnvList,
|
||||
volList: $scope.volList,
|
||||
advancedEnvMode: $scope.advancedEnvMode
|
||||
advancedEnvMode: $scope.advancedEnvMode,
|
||||
ports: $scope.currentPorts || {}
|
||||
};
|
||||
|
||||
|
||||
@@ -2088,6 +2137,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
title: 'Settings Saved',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts || {});
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -2213,22 +2263,55 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
$("#commandModal").modal("show");
|
||||
};
|
||||
|
||||
// Port editing functionality
|
||||
$scope.showPortEditModal = function() {
|
||||
// Initialize current ports from container data
|
||||
// Port editing (in Container Settings modal)
|
||||
$scope.initSettingsPortsFromContainer = function () {
|
||||
$scope.currentPorts = {};
|
||||
if ($scope.ports) {
|
||||
for (var iport in $scope.ports) {
|
||||
var eport = $scope.ports[iport];
|
||||
if (eport && eport.length > 0) {
|
||||
var src = $scope.serverPortMap && Object.keys($scope.serverPortMap).length
|
||||
? $scope.serverPortMap
|
||||
: ($scope.ports || {});
|
||||
if (src && typeof src === 'object') {
|
||||
for (var iport in src) {
|
||||
if (!Object.prototype.hasOwnProperty.call(src, iport)) {
|
||||
continue;
|
||||
}
|
||||
var eport = src[iport];
|
||||
if (angular.isArray(eport) && eport.length > 0 && eport[0] && eport[0].HostPort) {
|
||||
$scope.currentPorts[iport] = eport[0].HostPort;
|
||||
} else if (eport !== undefined && eport !== null && eport !== '') {
|
||||
$scope.currentPorts[iport] = String(eport);
|
||||
}
|
||||
}
|
||||
}
|
||||
$("#portEditModal").modal("show");
|
||||
$scope.initialPortsForSettings = angular.copy($scope.currentPorts);
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function() {
|
||||
$scope.portsJsonSnapshot = function (obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return '{}';
|
||||
}
|
||||
var keys = Object.keys(obj).sort();
|
||||
var flat = {};
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
var v = obj[k];
|
||||
flat[k] = v === null || v === undefined ? '' : String(v);
|
||||
}
|
||||
return JSON.stringify(flat);
|
||||
};
|
||||
|
||||
$scope.portsDirty = function () {
|
||||
return $scope.portsJsonSnapshot($scope.currentPorts) !== $scope.portsJsonSnapshot($scope.initialPortsForSettings);
|
||||
};
|
||||
|
||||
$scope.showPortEditModal = function () {
|
||||
$scope.initSettingsPortsFromContainer();
|
||||
$("#settings").modal("show");
|
||||
};
|
||||
|
||||
$scope.addNewPortMapping = function () {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
var containerPort = prompt('Enter container port (e.g., 80/tcp):');
|
||||
if (containerPort) {
|
||||
$scope.currentPorts[containerPort] = '';
|
||||
@@ -2236,14 +2319,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removePortMapping = function(containerPort) {
|
||||
$scope.removePortMapping = function (containerPort) {
|
||||
if (!$scope.envConfirmation) {
|
||||
return;
|
||||
}
|
||||
if (confirm('Are you sure you want to remove this port mapping?')) {
|
||||
delete $scope.currentPorts[containerPort];
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updatePortMappings = function() {
|
||||
$("#portEditLoading").show();
|
||||
$scope.updatePortMappings = function () {
|
||||
$scope.updatingPorts = true;
|
||||
|
||||
var url = "/docker/updateContainerPorts";
|
||||
@@ -2259,18 +2344,16 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
};
|
||||
|
||||
$http.post(url, data, config).then(function(response) {
|
||||
$("#portEditLoading").hide();
|
||||
$scope.updatingPorts = false;
|
||||
|
||||
if (response.data.status === 1) {
|
||||
$("#portEditModal").modal("hide");
|
||||
// Refresh container status and ports
|
||||
$scope.refreshContainerInfo();
|
||||
$("#settings").modal("hide");
|
||||
new PNotify({
|
||||
title: 'Success',
|
||||
text: 'Port mappings updated successfully',
|
||||
type: 'success'
|
||||
});
|
||||
$scope.refreshContainerInfo();
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
@@ -2279,11 +2362,10 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
|
||||
});
|
||||
}
|
||||
}, function(error) {
|
||||
$("#portEditLoading").hide();
|
||||
$scope.updatingPorts = false;
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: 'Error updating port mappings: ' + error.data.error_message,
|
||||
text: 'Error updating port mappings: ' + (error.data && error.data.error_message ? error.data.error_message : 'unknown'),
|
||||
type: 'error'
|
||||
});
|
||||
});
|
||||
@@ -2388,12 +2470,55 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
$scope.showingSearch = false;
|
||||
$("#searchResult").hide();
|
||||
|
||||
function dockerHistoryFormatSize(raw) {
|
||||
var n = parseInt(raw, 10);
|
||||
if (isNaN(n) || n < 0) {
|
||||
return (raw === undefined || raw === null) ? '' : String(raw);
|
||||
}
|
||||
if (n === 0) {
|
||||
return '0 B';
|
||||
}
|
||||
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
var i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
|
||||
var v = n / Math.pow(1024, i);
|
||||
var s = (i === 0) ? String(v) : (Math.round(v * 10) / 10).toFixed(1);
|
||||
return s + ' ' + units[i];
|
||||
}
|
||||
|
||||
function dockerHistoryFormatCreated(raw) {
|
||||
if (raw === undefined || raw === null) {
|
||||
return '';
|
||||
}
|
||||
var n = Number(raw);
|
||||
if (isNaN(n)) {
|
||||
return String(raw);
|
||||
}
|
||||
var sec = n;
|
||||
if (n > 1e14) {
|
||||
sec = Math.floor(n / 1e9);
|
||||
} else if (n > 1e12) {
|
||||
sec = Math.floor(n / 1e6);
|
||||
}
|
||||
var d = new Date(sec * 1000);
|
||||
if (isNaN(d.getTime())) {
|
||||
return String(raw);
|
||||
}
|
||||
return d.toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, ' UTC');
|
||||
}
|
||||
|
||||
function dockerHistoryEnrichLayer(h) {
|
||||
var o = angular.extend({}, h);
|
||||
o.SizeHuman = dockerHistoryFormatSize(h.Size);
|
||||
o.CreatedDisplay = dockerHistoryFormatCreated(h.Created);
|
||||
return o;
|
||||
}
|
||||
|
||||
$scope.pullImage = function (image, tag) {
|
||||
function ListInitialDatas(response) {
|
||||
if (response.data.installImageStatus === 1) {
|
||||
new PNotify({
|
||||
title: 'Image pulled successfully',
|
||||
text: 'Reloading...',
|
||||
text: 'Running containers keep the old image until you recreate them from Docker > Containers. Reloading…',
|
||||
type: 'success'
|
||||
});
|
||||
location.reload()
|
||||
@@ -2424,7 +2549,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
url = "/docker/installImage";
|
||||
var data = {
|
||||
image: image,
|
||||
tag: tag
|
||||
tag: tag,
|
||||
force_update: true
|
||||
};
|
||||
var config = {
|
||||
headers: {
|
||||
@@ -2445,6 +2571,30 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
}
|
||||
|
||||
$scope.refreshLocalImage = function (rowId, imageName) {
|
||||
var sel = document.getElementById(String(rowId));
|
||||
if (!sel || sel.selectedIndex < 0) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var raw = sel.options[sel.selectedIndex].text;
|
||||
if (!raw) {
|
||||
new PNotify({
|
||||
title: 'Unable to complete request',
|
||||
text: 'Please select a tag for this image',
|
||||
type: 'info'
|
||||
});
|
||||
return;
|
||||
}
|
||||
var li = raw.lastIndexOf(':');
|
||||
var tag = li > -1 ? raw.substring(li + 1) : raw;
|
||||
$scope.pullImage(imageName, tag);
|
||||
}
|
||||
|
||||
$scope.searchImages = function () {
|
||||
console.log($scope.searchString);
|
||||
if (!$scope.searchString) {
|
||||
@@ -2580,7 +2730,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
$scope.getHistory = function (counter) {
|
||||
$('#imageLoading').show();
|
||||
var name = $("#" + counter).val()
|
||||
var sel = $("#" + counter);
|
||||
var name = (sel.find('option:selected').text() || sel.val() || '').trim();
|
||||
|
||||
url = "/docker/getImageHistory";
|
||||
|
||||
@@ -2600,7 +2751,8 @@ app.controller('manageImages', function ($scope, $http) {
|
||||
|
||||
if (response.data.imageHistoryStatus === 1) {
|
||||
$('#history').modal('show');
|
||||
$scope.historyList = response.data.history;
|
||||
var raw = response.data.history || [];
|
||||
$scope.historyList = raw.map(dockerHistoryEnrichLayer);
|
||||
}
|
||||
else {
|
||||
new PNotify({
|
||||
|
||||
Reference in New Issue
Block a user