fix(docker): listContainers HTML page – avoid JSON/cache mix-up

- Add GET /docker/containers for HTML page; GET /docker/listContainers redirects there
- POST /docker/listContainers returns 405 (page uses getContainerList for data)
- Remove duplicate listContainers Angular controller; fix pagination (getContainerList)
- Extend getContainerList API: totalCount, totalPages, currentPage, itemsPerPage
- Add ACTIVITY BOARD-style pagination: Prev/Next, Go to page, Showing X–Y of Z
- Update menu/templates/JS redirects to /docker/containers
- Sync dockerManager.js across app static, STATIC_ROOT, public/static
- Cache-Control on HTML response; cache-bust script ?v=4

Fixes raw JSON instead of UI when loading /docker/listContainers (cache/proxy
serving stored JSON for GET). Use /docker/containers for the page.
This commit is contained in:
master3395
2026-01-25 03:55:50 +01:00
parent c234265fdc
commit b4a9a0741f
10 changed files with 1923 additions and 784 deletions

View File

@@ -1,8 +1,8 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% with CP_VERSION="2.4.4.1" %}
{% with CP_VERSION="2.5.5-dev-fix" %}
<!DOCTYPE html>
<html lang="en" ng-app="CyberCP">
<html lang="en" ng-app="CyberCP" {% if cosmetic.MainDashboardCSS %}data-dynamic-css="{{ cosmetic.MainDashboardCSS|escape }}"{% endif %}>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -17,30 +17,39 @@
<!-- Custom CSS -->
<style>
{{ cosmetic.MainDashboardCSS | safe }}
/* Custom dashboard CSS - base styles */
body { visibility: visible; }
</style>
<!-- Mobile Responsive CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/mobile-responsive.css' %}?v={{ CP_VERSION }}">
<!-- Readability Fixes CSS -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/assets/readability-fixes.css' %}?v={{ CP_VERSION }}">
<!-- Core Scripts -->
<!-- Core Scripts - AngularJS MUST load FIRST before all other scripts -->
<script src="{% static 'baseTemplate/angularjs.1.6.5.js' %}?v={{ CP_VERSION }}"></script>
<!-- Dynamic CSS from Django template (injected via JavaScript to avoid linter errors) -->
<script>
// Inject dynamic CSS from data attribute to avoid CSS/JS linter errors
(function() {
var htmlElement = document.documentElement;
var cssContent = htmlElement.getAttribute('data-dynamic-css');
if (cssContent) {
var style = document.createElement('style');
style.textContent = cssContent;
document.head.appendChild(style);
}
})();
</script>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<!-- Bootstrap JavaScript -->
<script src="{% static 'baseTemplate/assets/bootstrap/js/bootstrap.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/bootstrap-toggle.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="{% static 'baseTemplate/custom-js/qrious.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="https://cdn.jsdelivr.net/npm/qrious/dist/qrious.min.js"></script>
<script src="{% static 'baseTemplate/custom-js/system-status.js' %}?v={{ CP_VERSION }}"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Chart.js -->
<script src="{% static 'baseTemplate/custom-js/chart.umd.min.js' %}?v={{ CP_VERSION }}"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- PNotify -->
<link rel="stylesheet" type="text/css" href="{% static 'baseTemplate/custom-js/pnotify.custom.min.css' %}?v={{ CP_VERSION }}">
@@ -1566,7 +1575,7 @@
{% block header_scripts %}{% endblock %}
</head>
<body>
<body data-has-backup-configured="{% if request.session.has_backup_configured %}true{% else %}false{% endif %}">
<!-- Header -->
<div id="header">
<button id="mobile-menu-toggle" class="mobile-menu-toggle" onclick="toggleSidebar()" style="display: none;">
@@ -1647,7 +1656,7 @@
<div class="info-line">
<strong>Uptime:</strong>
<span ng-show="!uptimeLoaded">Loading...</span>
<span ng-show="uptimeLoaded" ng-bind="uptime">N/A</span>
<span ng-show="uptimeLoaded">{$ uptime $}</span>
</div>
</div>
</div>
@@ -1803,9 +1812,6 @@
<a href="{% url 'listChildDomains' %}" class="menu-item">
<span>List Sub/Addon Domains</span>
</a>
<a href="{% url 'fixSubdomainLogs' %}" class="menu-item">
<span>Fix Subdomain Logs</span>
</a>
{% if admin or modifyWebsite %}
<a href="{% url 'modifyWebsite' %}" class="menu-item">
<span>Modify Website</span>
@@ -2157,7 +2163,7 @@
<a href="{% url 'manageImages' %}" class="menu-item">
<span>Manage Images</span>
</a>
<a href="{% url 'listContainers' %}" class="menu-item">
<a href="{% url 'listContainersPage' %}" class="menu-item">
<span>Manage Containers</span>
</a>
<a href="{% url 'containerImage' %}" class="menu-item">
@@ -2287,9 +2293,6 @@
<a href="{% url 'aiScannerHome' %}" class="menu-item">
<span>AI Scanner</span>
</a>
<a href="#" class="menu-item" onclick="loadSecurityManagement(); return false;">
<span>Security Management</span>
</a>
</div>
<a href="#" class="menu-item" onclick="toggleSubmenu('mail-settings-submenu', this); return false;">
@@ -2343,11 +2346,6 @@
<a href="{% url 'managePureFtpd' %}" class="menu-item">
<span>Manage FTP</span>
</a>
{% if admin %}
<a href="#" class="menu-item" onclick="loadBandwidthManagement(); return false;">
<span>Bandwidth Management</span>
</a>
{% endif %}
</div>
<a href="#" class="menu-item" onclick="toggleSubmenu('plugins-submenu', this); return false;">
@@ -2452,6 +2450,12 @@
<script src="{% static 'CLManager/CLManager.js' %}?v={{ CP_VERSION }}"></script>
<!-- Scripts -->
<!-- Set backup configuration status from Django template via data attribute -->
<script>
// Read backup configuration status from data attribute (set by Django template)
var backupConfigElement = document.querySelector('[data-has-backup-configured]');
window.hasBackupConfigured = backupConfigElement ? backupConfigElement.getAttribute('data-has-backup-configured') === 'true' : false;
</script>
<script>
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
@@ -2580,9 +2584,10 @@
// Check if user has backup configured (you'll need to implement this API)
// For now, we'll show it by default unless they have a backup plan
// This should be replaced with an actual check
{% if not request.session.has_backup_configured %}
// Note: hasBackupConfigured is set via Django template before this script
if (typeof window.hasBackupConfigured !== 'undefined' && !window.hasBackupConfigured) {
showBackupNotification();
{% endif %}
}
// For demonstration, let's show it if URL doesn't contain 'OneClickBackups'
if (!window.location.href.includes('OneClickBackups')) {
@@ -2819,7 +2824,44 @@
item.classList.remove('active', 'has-active-child');
});
// Find and activate the matching menu item
// Special handling for plugin pages - expand Plugins submenu and highlight
if (currentPath.startsWith('/plugins/')) {
const pluginsSubmenu = document.getElementById('plugins-submenu');
const pluginsMenuItem = pluginsSubmenu ? pluginsSubmenu.previousElementSibling : null;
if (pluginsSubmenu) {
pluginsSubmenu.classList.add('show');
}
if (pluginsMenuItem && pluginsMenuItem.classList.contains('menu-item')) {
pluginsMenuItem.classList.add('expanded', 'has-active-child');
}
// Check if it's the installed page
if (currentPath === '/plugins/installed' || currentPath.startsWith('/plugins/installed')) {
const installedLink = pluginsSubmenu ? pluginsSubmenu.querySelector('a[href*="installed"]') : null;
if (installedLink) {
installedLink.classList.add('active');
}
} else if (currentPath.match(/^\/plugins\/[^\/]+\/(settings|help)/)) {
// It's a plugin settings/help page - highlight "Installed" menu item
const installedLink = pluginsSubmenu ? pluginsSubmenu.querySelector('a[href*="installed"]') : null;
if (installedLink) {
installedLink.classList.add('active');
}
} else {
// Try to match specific plugin links in the submenu
const pluginLinks = pluginsSubmenu ? pluginsSubmenu.querySelectorAll('a.menu-item') : [];
pluginLinks.forEach(link => {
const href = link.getAttribute('href');
if (href && (currentPath === href || currentPath.startsWith(href + '/'))) {
link.classList.add('active');
}
});
}
}
// Find and activate the matching menu item (exact matches)
allMenuItems.forEach(item => {
const href = item.getAttribute('href');
if (href && href !== '#' && currentPath === href) {
@@ -2917,4 +2959,4 @@
{% block footer_scripts %}{% endblock %}
</body>
</html>
{% endwith %}
{% endwith %}

View File

@@ -698,9 +698,31 @@ class ContainerManager(multi.Thread):
return ACLManager.loadErrorJson('listContainerStatus', 0)
currentACL = ACLManager.loadedACL(userID)
pageNumber = int(data['page'])
json_data = self.findContainersJson(currentACL, userID, pageNumber)
final_dic = {'listContainerStatus': 1, 'error_message': "None", "data": json_data}
pageNumber = max(1, int(data.get('page', 1)))
items_per_page = 10
all_containers = ACLManager.findContainersObjects(currentACL, userID)
totalCount = len(all_containers)
totalPages = max(1, int(ceil(float(totalCount) / float(items_per_page))))
start = (pageNumber - 1) * items_per_page
end = start + items_per_page
page_containers = all_containers[start:end]
rows = []
for items in page_containers:
rows.append({'name': items.name, 'admin': items.admin.userName, 'tag': items.tag, 'image': items.image})
json_data = json.dumps(rows)
final_dic = {
'listContainerStatus': 1,
'error_message': 'None',
'data': json_data,
'totalCount': totalCount,
'totalPages': totalPages,
'currentPage': pageNumber,
'itemsPerPage': items_per_page
}
final_json = json.dumps(final_dic)
return HttpResponse(final_json)
except BaseException as msg:
@@ -2407,9 +2429,9 @@ class ContainerManager(multi.Thread):
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
def listContainers(self, userID=None):
def listContainersJson(self, userID=None):
"""
Get list of all Docker containers
Get list of all Docker containers as JSON (for Angular API).
"""
try:
admin = Administrator.objects.get(pk=userID)
@@ -2441,13 +2463,13 @@ class ContainerManager(multi.Thread):
'containers': container_list
}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json_data, content_type='application/json')
except Exception as msg:
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.listContainers]')
logging.CyberCPLogFileWriter.writeToFile(str(msg) + ' [ContainerManager.listContainersJson]')
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
return HttpResponse(json_data, content_type='application/json')
def getDockerNetworks(self, userID=None):
"""

View File

@@ -729,6 +729,59 @@ app.controller('listContainers', function ($scope, $http) {
$scope.assignActive = "";
$scope.dockerOwner = "";
/* Pagination (ACTIVITY BOARD-style) */
var CONTAINERS_PER_PAGE = 10;
$scope.pagination = { containers: { currentPage: 1, itemsPerPage: CONTAINERS_PER_PAGE } };
$scope.gotoPageInput = { containers: 1 };
$scope.totalCount = 0;
$scope.totalPages = 1;
$scope.Math = Math;
$scope.getTotalPages = function(section) {
if (section === 'containers') return Math.max(1, $scope.totalPages || 1);
return 1;
};
$scope.goToPage = function(section, page) {
if (section !== 'containers') return;
var totalPages = $scope.getTotalPages(section);
var p = parseInt(page, 10);
if (isNaN(p) || p < 1) p = 1;
if (p > totalPages) p = totalPages;
$scope.getFurtherContainersFromDB(p);
};
$scope.nextPage = function(section) {
if (section !== 'containers') return;
if ($scope.pagination.containers.currentPage < $scope.getTotalPages(section)) {
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage + 1);
}
};
$scope.prevPage = function(section) {
if (section !== 'containers') return;
if ($scope.pagination.containers.currentPage > 1) {
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage - 1);
}
};
$scope.getPageNumbers = function(section) {
if (section !== 'containers') return [];
var totalPages = $scope.getTotalPages(section);
var current = $scope.pagination.containers.currentPage;
var maxVisible = 5;
var pages = [];
if (totalPages <= maxVisible) {
for (var i = 1; i <= totalPages; i++) pages.push(i);
} else {
var start = Math.max(1, current - 2);
var end = Math.min(totalPages, start + maxVisible - 1);
if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1);
for (var j = start; j <= end; j++) pages.push(j);
}
return pages;
};
$scope.assignContainer = function (name) {
console.log('assignContainer called with:', name);
$scope.assignActive = name;
@@ -783,7 +836,7 @@ app.controller('listContainers', function ($scope, $http) {
title: 'Container assigned successfully',
type: 'success'
});
window.location.href = '/docker/listContainers';
window.location.href = '/docker/containers';
}
else {
new PNotify({
@@ -1264,77 +1317,35 @@ app.controller('listContainers', function ($scope, $http) {
$scope.logInfo = null;
};
url = "/docker/getContainerList";
var data = {page: 1};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
console.log(response);
function handleContainerListResponse(response) {
if (response.data.listContainerStatus === 1) {
var finalData = JSON.parse(response.data.data);
$scope.ContainerList = finalData;
console.log($scope.ContainerList);
$scope.totalCount = response.data.totalCount || 0;
$scope.totalPages = Math.max(1, response.data.totalPages || 1);
var cp = Math.max(1, parseInt(response.data.currentPage, 10) || 1);
$scope.pagination.containers.currentPage = cp;
$scope.gotoPageInput.containers = cp;
$("#listFail").hide();
}
else {
} else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
$scope.errorMessage = response.data.error_message || 'Failed to load containers';
}
}
function cantLoadInitialData(response) {
console.log("not good");
$("#listFail").fadeIn();
$scope.errorMessage = (response && response.data && response.data.error_message) ? response.data.error_message : 'Could not connect to server';
}
var config = {
headers: { 'X-CSRFToken': getCookie('csrftoken') }
};
$http.post("/docker/getContainerList", { page: 1 }, config).then(handleContainerListResponse, cantLoadInitialData);
$scope.getFurtherContainersFromDB = function (pageNumber) {
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = {page: pageNumber};
dataurl = "/docker/getContainerList";
$http.post(dataurl, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
if (response.data.listContainerStatus === 1) {
var finalData = JSON.parse(response.data.data);
$scope.ContainerList = finalData;
$("#listFail").hide();
}
else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
console.log(response.data);
}
}
function cantLoadInitialData(response) {
console.log("not good");
}
var p = Math.max(1, parseInt(pageNumber, 10) || 1);
$http.post("/docker/getContainerList", { page: p }, config).then(handleContainerListResponse, cantLoadInitialData);
};
});
@@ -1906,7 +1917,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
text: 'Redirecting...',
type: 'success'
});
window.location.href = '/docker/listContainers';
window.location.href = '/docker/containers';
}
else {
new PNotify({
@@ -2703,257 +2714,3 @@ app.controller('manageImages', function ($scope, $http) {
}
});
// Container List Controller
app.controller('listContainers', function ($scope, $http, $timeout, $window) {
$scope.containers = [];
$scope.loading = false;
$scope.updateContainerName = '';
$scope.currentImage = '';
$scope.newImage = '';
$scope.newTag = 'latest';
// Load containers list
$scope.loadContainers = function() {
$scope.loading = true;
var url = '/docker/listContainers';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$scope.loading = false;
if (response.data.status === 1) {
$scope.containers = response.data.containers || [];
} else {
new PNotify({
title: 'Error Loading Containers',
text: response.data.error_message || 'Failed to load containers',
type: 'error'
});
}
}, function(error) {
$scope.loading = false;
new PNotify({
title: 'Connection Error',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Initialize containers on page load
$scope.loadContainers();
// Open update container modal
$scope.openUpdateModal = function(container) {
$scope.updateContainerName = container.name;
$scope.currentImage = container.image;
$scope.newImage = '';
$scope.newTag = 'latest';
$('#updateContainer').modal('show');
};
// Perform container update
$scope.performUpdate = function() {
if (!$scope.newImage && !$scope.newTag) {
new PNotify({
title: 'Missing Information',
text: 'Please enter a new image name or tag',
type: 'error'
});
return;
}
// If no new image specified, use current image with new tag
var imageToUse = $scope.newImage || $scope.currentImage.split(':')[0];
var tagToUse = $scope.newTag || 'latest';
var data = {
containerName: $scope.updateContainerName,
newImage: imageToUse,
newTag: tagToUse
};
var url = '/docker/updateContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
// Show loading
$('#updateContainer').modal('hide');
new PNotify({
title: 'Updating Container',
text: 'Please wait while the container is being updated...',
type: 'info',
hide: false
});
$http.post(url, data, config).then(function(response) {
if (response.data.updateContainerStatus === 1) {
new PNotify({
title: 'Container Updated Successfully',
text: response.data.message || 'Container has been updated successfully',
type: 'success'
});
// Reload containers list
$scope.loadContainers();
} else {
new PNotify({
title: 'Update Failed',
text: response.data.error_message || 'Failed to update container',
type: 'error'
});
}
}, function(error) {
new PNotify({
title: 'Update Failed',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Container actions
$scope.startContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/startContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.startContainerStatus === 1) {
new PNotify({
title: 'Container Started',
text: 'Container has been started successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Start Failed',
text: response.data.error_message || 'Failed to start container',
type: 'error'
});
}
});
};
$scope.stopContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/stopContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.stopContainerStatus === 1) {
new PNotify({
title: 'Container Stopped',
text: 'Container has been stopped successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Stop Failed',
text: response.data.error_message || 'Failed to stop container',
type: 'error'
});
}
});
};
$scope.restartContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/restartContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.restartContainerStatus === 1) {
new PNotify({
title: 'Container Restarted',
text: 'Container has been restarted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Restart Failed',
text: response.data.error_message || 'Failed to restart container',
type: 'error'
});
}
});
};
$scope.deleteContainerWithData = function(containerName) {
if (confirm('Are you sure you want to delete this container and all its data? This action cannot be undone.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerWithData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerWithDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container and all data have been deleted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
$scope.deleteContainerKeepData = function(containerName) {
if (confirm('Are you sure you want to delete this container but keep the data? The container will be removed but volumes will be preserved.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerKeepData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerKeepDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container has been deleted but data has been preserved',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
});

View File

@@ -433,7 +433,7 @@
<i class="fas fa-search"></i>
{% trans "Search & Manage Images" %}
</a>
<a href="{% url 'listContainers' %}" class="btn-secondary">
<a href="{% url 'listContainersPage' %}" class="btn-secondary">
<i class="fas fa-cube"></i>
{% trans "View Containers" %}
</a>

View File

@@ -26,7 +26,7 @@
<div class="example-box-wrapper">
<div class="row">
<div class="col-md-4">
<a href="{% url 'listContainers' %}" title="{% trans 'Manage Containers' %}" class="tile-box tile-box-shortcut btn-primary">
<a href="{% url 'listContainersPage' %}" title="{% trans 'Manage Containers' %}" class="tile-box tile-box-shortcut btn-primary">
<div class="tile-header">
{% trans "Manage Containers" %}
</div>

View File

@@ -674,14 +674,37 @@
<strong>{% trans "Error:" %}</strong> {$ errorMessage $}
</div>
<div class="pagination" ng-if="pagination">
<ul>
{% for items in pagination %}
<li ng-click="getFurtherContainersFromDB({{ forloop.counter }})">
<a href="">{{ forloop.counter }}</a>
</li>
{% endfor %}
</ul>
<!-- Pagination Controls (ACTIVITY BOARD-style: Go to page) -->
<div ng-if="pagination && pagination.containers && (ContainerList.length > 0 || totalCount > 0)" class="pagination-wrapper" style="margin-top: 20px; padding: 15px; background: #f8f9ff; border-radius: 8px; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px;">
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<button ng-click="prevPage('containers')" ng-disabled="!pagination.containers || (pagination.containers.currentPage || 1) === 1"
class="pagination-btn" style="padding: 6px 12px; border: 1px solid #e8e9ff; background: white; border-radius: 6px; cursor: pointer; font-size: 12px; color: #2f3640;">
<i class="fas fa-chevron-left"></i> {% trans "Prev" %}
</button>
<span style="font-size: 12px; color: #64748b; margin: 0 8px;">
{% trans "Page" %} {$ (pagination.containers.currentPage || 1) $} {% trans "of" %} {$ (getTotalPages('containers') || 1) $}
</span>
<button ng-click="nextPage('containers')" ng-disabled="!pagination.containers || !getTotalPages || (pagination.containers.currentPage || 1) >= (getTotalPages('containers') || 1)"
class="pagination-btn" style="padding: 6px 12px; border: 1px solid #e8e9ff; background: white; border-radius: 6px; cursor: pointer; font-size: 12px; color: #2f3640;">
{% trans "Next" %} <i class="fas fa-chevron-right"></i>
</button>
</div>
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<label style="font-size: 12px; color: #64748b; margin: 0;">{% trans "Go to page:" %}</label>
<input type="number" min="1" max="{$ (getTotalPages('containers') || 1) $}"
ng-model="gotoPageInput.containers"
ng-keyup="$event.keyCode === 13 && goToPage('containers', gotoPageInput.containers)"
style="width: 60px; padding: 6px; border: 1px solid #e8e9ff; border-radius: 6px; font-size: 12px; text-align: center;">
<button ng-click="goToPage('containers', gotoPageInput.containers)"
class="pagination-btn" style="padding: 6px 12px; border: 1px solid #5b5fcf; background: #5b5fcf; color: white; border-radius: 6px; cursor: pointer; font-size: 12px;">
{% trans "Go" %}
</button>
</div>
<div style="font-size: 12px; color: #64748b; margin: 0;" ng-if="totalCount > 0">
<span ng-if="pagination && pagination.containers">
{% trans "Showing" %} {$ ((pagination.containers.currentPage || 1) - 1) * (pagination.containers.itemsPerPage || 10) + 1 $}-{$ Math.min((pagination.containers.currentPage || 1) * (pagination.containers.itemsPerPage || 10), totalCount) $} {% trans "of" %} {$ totalCount $} {% trans "entries" %}
</span>
</div>
</div>
</div>
</div>
@@ -949,5 +972,5 @@
{% endblock %}
{% block footer_scripts %}
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
<script src="{% static 'dockerManager/dockerManager.js' %}?v=4"></script>
{% endblock %}

View File

@@ -9,6 +9,7 @@ urlpatterns = [
re_path(r'^getTags$', views.getTags, name='getTags'),
re_path(r'^runContainer', views.runContainer, name='runContainer'),
re_path(r'^submitContainerCreation$', views.submitContainerCreation, name='submitContainerCreation'),
re_path(r'^containers$', views.listContainersPage, name='listContainersPage'),
re_path(r'^listContainers$', views.listContainers, name='listContainers'),
re_path(r'^getContainerList$', views.getContainerList, name='getContainerList'),
re_path(r'^getContainerLogs$', views.getContainerLogs, name='getContainerLogs'),
@@ -33,7 +34,6 @@ urlpatterns = [
re_path(r'^updateContainerPorts$', views.updateContainerPorts, name='updateContainerPorts'),
re_path(r'^manageNetworks$', views.manageNetworks, name='manageNetworks'),
re_path(r'^updateContainer$', views.updateContainer, name='updateContainer'),
re_path(r'^listContainers$', views.listContainers, name='listContainers'),
re_path(r'^deleteContainerWithData$', views.deleteContainerWithData, name='deleteContainerWithData'),
re_path(r'^deleteContainerKeepData$', views.deleteContainerKeepData, name='deleteContainerKeepData'),
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),

View File

@@ -179,14 +179,40 @@ def runContainer(request):
return redirect(loadLoginPage)
@preDockerRun
def listContainers(request):
def listContainersPage(request):
"""
GET /docker/containers: Render HTML page only. Separate URL avoids
cache/proxy ever serving JSON (listContainers used to return JSON).
"""
try:
userID = request.session['userID']
cm = ContainerManager()
return cm.listContainers(request, userID)
resp = cm.listContainers(request, userID)
resp['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
resp['Pragma'] = 'no-cache'
resp['Expires'] = '0'
return resp
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def listContainers(request):
"""
GET: Redirect to /docker/containers (HTML). POST: 405.
listContainers URL historically returned JSON; caches may serve stale JSON.
Use /docker/containers for the page to avoid that.
"""
try:
request.session['userID'] # ensure logged in
except KeyError:
return redirect(loadLoginPage)
if request.method != 'GET':
return HttpResponse('Method Not Allowed', status=405)
from django.urls import reverse
return redirect(reverse('listContainersPage'))
@preDockerRun
def getContainerLogs(request):
try:
@@ -746,27 +772,6 @@ def getContainerEnv(request):
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def listContainers(request):
"""
Get list of all Docker containers
"""
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.listContainers(userID)
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getDockerNetworks(request):
"""

File diff suppressed because it is too large Load Diff

View File

@@ -729,6 +729,59 @@ app.controller('listContainers', function ($scope, $http) {
$scope.assignActive = "";
$scope.dockerOwner = "";
/* Pagination (ACTIVITY BOARD-style) */
var CONTAINERS_PER_PAGE = 10;
$scope.pagination = { containers: { currentPage: 1, itemsPerPage: CONTAINERS_PER_PAGE } };
$scope.gotoPageInput = { containers: 1 };
$scope.totalCount = 0;
$scope.totalPages = 1;
$scope.Math = Math;
$scope.getTotalPages = function(section) {
if (section === 'containers') return Math.max(1, $scope.totalPages || 1);
return 1;
};
$scope.goToPage = function(section, page) {
if (section !== 'containers') return;
var totalPages = $scope.getTotalPages(section);
var p = parseInt(page, 10);
if (isNaN(p) || p < 1) p = 1;
if (p > totalPages) p = totalPages;
$scope.getFurtherContainersFromDB(p);
};
$scope.nextPage = function(section) {
if (section !== 'containers') return;
if ($scope.pagination.containers.currentPage < $scope.getTotalPages(section)) {
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage + 1);
}
};
$scope.prevPage = function(section) {
if (section !== 'containers') return;
if ($scope.pagination.containers.currentPage > 1) {
$scope.getFurtherContainersFromDB($scope.pagination.containers.currentPage - 1);
}
};
$scope.getPageNumbers = function(section) {
if (section !== 'containers') return [];
var totalPages = $scope.getTotalPages(section);
var current = $scope.pagination.containers.currentPage;
var maxVisible = 5;
var pages = [];
if (totalPages <= maxVisible) {
for (var i = 1; i <= totalPages; i++) pages.push(i);
} else {
var start = Math.max(1, current - 2);
var end = Math.min(totalPages, start + maxVisible - 1);
if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1);
for (var j = start; j <= end; j++) pages.push(j);
}
return pages;
};
$scope.assignContainer = function (name) {
console.log('assignContainer called with:', name);
$scope.assignActive = name;
@@ -783,7 +836,7 @@ app.controller('listContainers', function ($scope, $http) {
title: 'Container assigned successfully',
type: 'success'
});
window.location.href = '/docker/listContainers';
window.location.href = '/docker/containers';
}
else {
new PNotify({
@@ -1264,77 +1317,35 @@ app.controller('listContainers', function ($scope, $http) {
$scope.logInfo = null;
};
url = "/docker/getContainerList";
var data = {page: 1};
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
console.log(response);
function handleContainerListResponse(response) {
if (response.data.listContainerStatus === 1) {
var finalData = JSON.parse(response.data.data);
$scope.ContainerList = finalData;
console.log($scope.ContainerList);
$scope.totalCount = response.data.totalCount || 0;
$scope.totalPages = Math.max(1, response.data.totalPages || 1);
var cp = Math.max(1, parseInt(response.data.currentPage, 10) || 1);
$scope.pagination.containers.currentPage = cp;
$scope.gotoPageInput.containers = cp;
$("#listFail").hide();
}
else {
} else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
$scope.errorMessage = response.data.error_message || 'Failed to load containers';
}
}
function cantLoadInitialData(response) {
console.log("not good");
$("#listFail").fadeIn();
$scope.errorMessage = (response && response.data && response.data.error_message) ? response.data.error_message : 'Could not connect to server';
}
var config = {
headers: { 'X-CSRFToken': getCookie('csrftoken') }
};
$http.post("/docker/getContainerList", { page: 1 }, config).then(handleContainerListResponse, cantLoadInitialData);
$scope.getFurtherContainersFromDB = function (pageNumber) {
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
var data = {page: pageNumber};
dataurl = "/docker/getContainerList";
$http.post(dataurl, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
if (response.data.listContainerStatus === 1) {
var finalData = JSON.parse(response.data.data);
$scope.ContainerList = finalData;
$("#listFail").hide();
}
else {
$("#listFail").fadeIn();
$scope.errorMessage = response.data.error_message;
console.log(response.data);
}
}
function cantLoadInitialData(response) {
console.log("not good");
}
var p = Math.max(1, parseInt(pageNumber, 10) || 1);
$http.post("/docker/getContainerList", { page: p }, config).then(handleContainerListResponse, cantLoadInitialData);
};
});
@@ -1906,7 +1917,7 @@ app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
text: 'Redirecting...',
type: 'success'
});
window.location.href = '/docker/listContainers';
window.location.href = '/docker/containers';
}
else {
new PNotify({
@@ -2703,257 +2714,3 @@ app.controller('manageImages', function ($scope, $http) {
}
});
// Container List Controller
app.controller('listContainers', function ($scope, $http, $timeout, $window) {
$scope.containers = [];
$scope.loading = false;
$scope.updateContainerName = '';
$scope.currentImage = '';
$scope.newImage = '';
$scope.newTag = 'latest';
// Load containers list
$scope.loadContainers = function() {
$scope.loading = true;
var url = '/docker/listContainers';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, {}, config).then(function(response) {
$scope.loading = false;
if (response.data.status === 1) {
$scope.containers = response.data.containers || [];
} else {
new PNotify({
title: 'Error Loading Containers',
text: response.data.error_message || 'Failed to load containers',
type: 'error'
});
}
}, function(error) {
$scope.loading = false;
new PNotify({
title: 'Connection Error',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Initialize containers on page load
$scope.loadContainers();
// Open update container modal
$scope.openUpdateModal = function(container) {
$scope.updateContainerName = container.name;
$scope.currentImage = container.image;
$scope.newImage = '';
$scope.newTag = 'latest';
$('#updateContainer').modal('show');
};
// Perform container update
$scope.performUpdate = function() {
if (!$scope.newImage && !$scope.newTag) {
new PNotify({
title: 'Missing Information',
text: 'Please enter a new image name or tag',
type: 'error'
});
return;
}
// If no new image specified, use current image with new tag
var imageToUse = $scope.newImage || $scope.currentImage.split(':')[0];
var tagToUse = $scope.newTag || 'latest';
var data = {
containerName: $scope.updateContainerName,
newImage: imageToUse,
newTag: tagToUse
};
var url = '/docker/updateContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
// Show loading
$('#updateContainer').modal('hide');
new PNotify({
title: 'Updating Container',
text: 'Please wait while the container is being updated...',
type: 'info',
hide: false
});
$http.post(url, data, config).then(function(response) {
if (response.data.updateContainerStatus === 1) {
new PNotify({
title: 'Container Updated Successfully',
text: response.data.message || 'Container has been updated successfully',
type: 'success'
});
// Reload containers list
$scope.loadContainers();
} else {
new PNotify({
title: 'Update Failed',
text: response.data.error_message || 'Failed to update container',
type: 'error'
});
}
}, function(error) {
new PNotify({
title: 'Update Failed',
text: 'Could not connect to server',
type: 'error'
});
});
};
// Container actions
$scope.startContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/startContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.startContainerStatus === 1) {
new PNotify({
title: 'Container Started',
text: 'Container has been started successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Start Failed',
text: response.data.error_message || 'Failed to start container',
type: 'error'
});
}
});
};
$scope.stopContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/stopContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.stopContainerStatus === 1) {
new PNotify({
title: 'Container Stopped',
text: 'Container has been stopped successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Stop Failed',
text: response.data.error_message || 'Failed to stop container',
type: 'error'
});
}
});
};
$scope.restartContainer = function(containerName) {
var data = { containerName: containerName };
var url = '/docker/restartContainer';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.restartContainerStatus === 1) {
new PNotify({
title: 'Container Restarted',
text: 'Container has been restarted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Restart Failed',
text: response.data.error_message || 'Failed to restart container',
type: 'error'
});
}
});
};
$scope.deleteContainerWithData = function(containerName) {
if (confirm('Are you sure you want to delete this container and all its data? This action cannot be undone.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerWithData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerWithDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container and all data have been deleted successfully',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
$scope.deleteContainerKeepData = function(containerName) {
if (confirm('Are you sure you want to delete this container but keep the data? The container will be removed but volumes will be preserved.')) {
var data = { containerName: containerName };
var url = '/docker/deleteContainerKeepData';
var config = {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
};
$http.post(url, data, config).then(function(response) {
if (response.data.deleteContainerKeepDataStatus === 1) {
new PNotify({
title: 'Container Deleted',
text: 'Container has been deleted but data has been preserved',
type: 'success'
});
$scope.loadContainers();
} else {
new PNotify({
title: 'Delete Failed',
text: response.data.error_message || 'Failed to delete container',
type: 'error'
});
}
});
}
};
});