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:
master3395
2026-04-11 20:31:58 +02:00
parent ca6cbb7ebd
commit 0519e797f5
6 changed files with 842 additions and 185 deletions

View File

@@ -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'}

View File

@@ -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({

View File

@@ -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;">&times;</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">

View File

@@ -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;">&times;</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;

View File

@@ -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) {
})
}
});

View File

@@ -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({