design overall

This commit is contained in:
usmannasir
2025-06-15 01:10:08 +05:00
parent 03e8bbbf54
commit f23b57053c
2075 changed files with 102714 additions and 25096 deletions

0
dockerManager/__init__.py Executable file → Normal file
View File

0
dockerManager/admin.py Executable file → Normal file
View File

0
dockerManager/apps.py Executable file → Normal file
View File

49
dockerManager/container.py Executable file → Normal file
View File

@@ -137,8 +137,13 @@ class ContainerManager(multi.Thread):
try:
name = self.name
if ACLManager.checkContainerOwnership(name, userID) != 1:
return ACLManager.loadError()
# Check if user is admin or has container access
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
# For non-admin users, check container ownership
if ACLManager.checkContainerOwnership(name, userID) != 1:
return ACLManager.loadError()
# Admin users can access any container, including ones not in database
client = docker.from_env()
dockerAPI = docker.APIClient()
@@ -149,25 +154,37 @@ class ContainerManager(multi.Thread):
return HttpResponse("Container not found")
data = {}
con = Containers.objects.get(name=name)
data['name'] = name
data['image'] = con.image + ":" + con.tag
data['ports'] = json.loads(con.ports)
data['cid'] = con.cid
data['envList'] = json.loads(con.env)
data['volList'] = json.loads(con.volumes)
try:
con = Containers.objects.get(name=name)
data['name'] = name
data['image'] = con.image + ":" + con.tag
data['ports'] = json.loads(con.ports)
data['cid'] = con.cid
data['envList'] = json.loads(con.env)
data['volList'] = json.loads(con.volumes)
data['memoryLimit'] = con.memory
if con.startOnReboot == 1:
data['startOnReboot'] = 'true'
data['restartPolicy'] = "Yes"
else:
data['startOnReboot'] = 'false'
data['restartPolicy'] = "No"
except Containers.DoesNotExist:
# Container exists in Docker but not in database
data['name'] = name
data['image'] = container.image.tags[0] if container.image.tags else "Unknown"
data['ports'] = {}
data['cid'] = container.id
data['envList'] = {}
data['volList'] = {}
data['memoryLimit'] = 512
data['startOnReboot'] = 'false'
data['restartPolicy'] = "No"
stats = container.stats(decode=False, stream=False)
logs = container.logs(stream=True)
data['status'] = container.status
data['memoryLimit'] = con.memory
if con.startOnReboot == 1:
data['startOnReboot'] = 'true'
data['restartPolicy'] = "Yes"
else:
data['startOnReboot'] = 'false'
data['restartPolicy'] = "No"
if 'usage' in stats['memory_stats']:
# Calculate Usage

0
dockerManager/decorators.py Executable file → Normal file
View File

0
dockerManager/dockerInstall.py Executable file → Normal file
View File

0
dockerManager/migrations/__init__.py Executable file → Normal file
View File

0
dockerManager/models.py Executable file → Normal file
View File

0
dockerManager/pluginManager.py Executable file → Normal file
View File

0
dockerManager/signals.py Executable file → Normal file
View File

142
dockerManager/static/dockerManager/dockerManager.js Executable file → Normal file
View File

@@ -110,7 +110,7 @@ app.controller('dockerImages', function ($scope) {
/* Java script code to install Container */
app.controller('runContainer', function ($scope, $http) {
$scope.containerCreationLoading = true;
$scope.containerCreationLoading = false;
$scope.installationDetailsForm = false;
$scope.installationProgress = true;
$scope.errorMessageBox = true;
@@ -120,6 +120,10 @@ app.controller('runContainer', function ($scope, $http) {
$scope.volList = {};
$scope.volListNumber = 0;
$scope.eport = {};
$scope.iport = {};
$scope.portType = {};
$scope.envList = {};
$scope.addVolField = function () {
$scope.volList[$scope.volListNumber] = {'dest': '', 'src': ''};
$scope.volListNumber = $scope.volListNumber + 1;
@@ -137,6 +141,37 @@ app.controller('runContainer', function ($scope, $http) {
var statusFile;
// Watch for changes to validate ports
$scope.$watchGroup(['name', 'dockerOwner', 'memory'], function() {
$scope.updateFormValidity();
});
$scope.$watch('eport', function() {
$scope.updateFormValidity();
}, true);
$scope.formIsValid = false;
$scope.updateFormValidity = function() {
// Basic required fields
if (!$scope.name || !$scope.dockerOwner || !$scope.memory) {
$scope.formIsValid = false;
return;
}
// Check if all port mappings are filled (if they exist)
if ($scope.portType && Object.keys($scope.portType).length > 0) {
for (var port in $scope.portType) {
if (!$scope.eport || !$scope.eport[port]) {
$scope.formIsValid = false;
return;
}
}
}
$scope.formIsValid = true;
};
$scope.createContainer = function () {
$scope.containerCreationLoading = true;
@@ -239,13 +274,43 @@ app.controller('runContainer', function ($scope, $http) {
app.controller('listContainers', function ($scope, $http) {
$scope.activeLog = "";
$scope.assignActive = "";
$scope.dockerOwner = "";
$scope.assignContainer = function (name) {
$("#assign").modal("show");
console.log('assignContainer called with:', name);
$scope.assignActive = name;
console.log('assignActive set to:', $scope.assignActive);
$("#assign").modal("show");
};
// Test function to verify scope is working
$scope.testScope = function() {
alert('Scope is working! assignActive: ' + $scope.assignActive + ', dockerOwner: ' + $scope.dockerOwner);
};
$scope.submitAssignContainer = function () {
console.log('submitAssignContainer called');
console.log('dockerOwner:', $scope.dockerOwner);
console.log('assignActive:', $scope.assignActive);
if (!$scope.dockerOwner) {
new PNotify({
title: 'Error',
text: 'Please select an owner',
type: 'error'
});
return;
}
if (!$scope.assignActive) {
new PNotify({
title: 'Error',
text: 'No container selected',
type: 'error'
});
return;
}
url = "/docker/assignContainer";
var data = {name: $scope.assignActive, admin: $scope.dockerOwner};
@@ -507,11 +572,35 @@ app.controller('listContainers', function ($scope, $http) {
/* Java script code for containerr home page */
app.controller('viewContainer', function ($scope, $http) {
app.controller('viewContainer', function ($scope, $http, $interval, $timeout) {
$scope.cName = "";
$scope.status = "";
$scope.savingSettings = false;
$scope.loadingTop = false;
$scope.statusInterval = null;
$scope.statsInterval = null;
// Auto-refresh status every 5 seconds
$scope.startStatusMonitoring = function() {
$scope.statusInterval = $interval(function() {
$scope.refreshStatus(true); // Silent refresh
}, 5000);
};
// Stop monitoring on scope destroy
$scope.$on('$destroy', function() {
if ($scope.statusInterval) {
$interval.cancel($scope.statusInterval);
}
if ($scope.statsInterval) {
$interval.cancel($scope.statsInterval);
}
});
// Start monitoring when controller loads
$timeout(function() {
$scope.startStatusMonitoring();
}, 1000);
$scope.recreate = function () {
(new PNotify({
@@ -674,7 +763,7 @@ app.controller('viewContainer', function ($scope, $http) {
})
};
$scope.refreshStatus = function () {
$scope.refreshStatus = function (silent) {
url = "/docker/getContainerStatus";
var data = {name: $scope.cName};
var config = {
@@ -686,26 +775,47 @@ app.controller('viewContainer', function ($scope, $http) {
$http.post(url, data, config).then(ListInitialData, cantLoadInitialData);
function ListInitialData(response) {
if (response.data.containerStatus === 1) {
console.log(response.data.status);
var oldStatus = $scope.status;
$scope.status = response.data.status;
// Animate status change
if (oldStatus !== $scope.status && !silent) {
// Add animation class
var statusBadge = document.querySelector('.status-badge');
if (statusBadge) {
statusBadge.classList.add('status-changed');
setTimeout(function() {
statusBadge.classList.remove('status-changed');
}, 600);
}
new PNotify({
title: 'Status Updated',
text: 'Container is now ' + $scope.status,
type: 'info',
delay: 2000
});
}
}
else {
new PNotify({
title: 'Unable to complete request',
text: response.data.error_message,
type: 'error'
});
if (!silent) {
new PNotify({
title: 'Unable to complete request',
text: response.data.error_message,
type: 'error'
});
}
}
}
function cantLoadInitialData(response) {
PNotify.error({
title: 'Unable to complete request',
text: "Problem in connecting to server"
});
if (!silent) {
PNotify.error({
title: 'Unable to complete request',
text: "Problem in connecting to server"
});
}
}
};
$scope.addVolField = function () {

585
dockerManager/templates/dockerManager/images.html Executable file → Normal file
View File

@@ -1,63 +1,556 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Docker Manage Images - CyberPanel" %}{% endblock %}
{% block title %}{% trans "Docker Images - CyberPanel" %}{% endblock %}
{% block content %}
{% load static %}
{% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} -->
<div class="container" ng-controller="manageImages">
<div id="page-title">
<h2>{% trans "Create new container" %}
<a href="{% url 'manageImages' %}" class="btn btn-info pull-right" title="{% trans 'Search new images and manage existing ones' %}">Manage Images</a>
</h2>
</div>
<style>
.modern-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
<div class="panel">
<div class="panel-body">
<h3 class="title-hero">
{% trans "Locally Available Images" %} <img id="imageLoading" src="/static/images/loading.gif" style="display: none;">
</h3><br>
.page-header {
text-align: center;
margin-bottom: 3rem;
padding: 3rem 0;
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
border-radius: 20px;
animation: fadeInDown 0.5s ease-out;
position: relative;
overflow: hidden;
}
.page-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 70% 30%, rgba(91, 95, 207, 0.15) 0%, transparent 50%);
animation: rotate 30s linear infinite;
}
.page-title {
font-size: 3rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
position: relative;
z-index: 1;
}
.docker-icon {
width: 60px;
height: 60px;
background: white;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.page-subtitle {
font-size: 1.25rem;
color: #64748b;
margin-bottom: 1.5rem;
position: relative;
z-index: 1;
}
.header-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
position: relative;
z-index: 1;
}
.btn-primary {
background: #5b5fcf;
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-size: 0.875rem;
}
.btn-primary:hover {
background: #4547a9;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(91, 95, 207, 0.4);
color: white;
}
.btn-secondary {
background: #fff;
color: #5b5fcf;
border: 1px solid #e8e9ff;
padding: 0.75rem 2rem;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-size: 0.875rem;
}
.btn-secondary:hover {
background: #f8f9ff;
border-color: #5b5fcf;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.2);
}
.main-card {
background: white;
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 10px 40px rgba(0,0,0,0.08);
border: 1px solid #e8e9ff;
overflow: hidden;
margin-bottom: 2rem;
animation: fadeInUp 0.5s ease-out;
}
.card-header {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
padding: 1.5rem 2rem;
border-bottom: 1px solid #e8e9ff;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.card-body {
padding: 2rem;
}
.images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.image-card {
background: #f8f9ff;
border: 1px solid #e8e9ff;
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.image-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
background: linear-gradient(135deg, rgba(91, 95, 207, 0.1) 0%, transparent 100%);
border-radius: 0 0 0 100%;
transition: all 0.3s ease;
}
.image-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(91, 95, 207, 0.15);
border-color: #5b5fcf;
}
.image-card:hover::before {
width: 150px;
height: 150px;
}
.image-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.image-icon {
width: 48px;
height: 48px;
background: white;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
color: #5b5fcf;
font-size: 1.5rem;
}
.image-name {
flex: 1;
}
.image-title {
font-size: 1.125rem;
font-weight: 600;
color: #1e293b;
margin: 0;
word-break: break-word;
}
.image-subtitle {
font-size: 0.75rem;
color: #64748b;
margin-top: 0.25rem;
}
.tag-selector {
margin: 1rem 0;
}
.tag-label {
font-size: 0.75rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
display: block;
}
.tag-select {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #e8e9ff;
border-radius: 8px;
background: white;
color: #1e293b;
font-size: 0.875rem;
transition: all 0.3s ease;
cursor: pointer;
}
.tag-select:focus {
outline: none;
border-color: #5b5fcf;
box-shadow: 0 0 0 3px rgba(91, 95, 207, 0.1);
}
.image-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.5rem;
}
.action-btn {
flex: 1;
padding: 0.75rem 1rem;
border-radius: 8px;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border: none;
}
.action-btn.create {
background: #5b5fcf;
color: white;
}
.action-btn.create:hover {
background: #4547a9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.3);
}
.action-btn.details {
background: white;
color: #5b5fcf;
border: 1px solid #e8e9ff;
}
.action-btn.details:hover {
background: #f8f9ff;
border-color: #5b5fcf;
transform: translateY(-2px);
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
}
.empty-icon {
width: 80px;
height: 80px;
background: #f8f9ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
color: #5b5fcf;
font-size: 2rem;
}
.empty-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 0.5rem;
}
.empty-text {
color: #64748b;
margin-bottom: 2rem;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 3px solid #e8e9ff;
border-top-color: #5b5fcf;
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
margin-left: 1rem;
}
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
border-radius: 20px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.badge-count {
background: #e0e7ff;
color: #5b5fcf;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.page-title {
font-size: 2rem;
}
<div class="example-box-wrapper">
<table cellpadding="0" cellspacing="0" border="0" class="table table-striped table-bordered" id="imageList">
<thead>
<tr>
<th>Name (Installed)</th>
<th>Tags</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for name, image in images.items %}
<tr>
<td>{{image.name}}</td>
<td>
<select class="form-control tagList" id="{{forloop.counter}}" ng-model="imageTag['{{ image.name2 }}']">
{% for tag in image.tags%}
<option>{{tag}}</option>
{% endfor %}
</select>
</td>
<td>
<a class="btn btn-primary" ng-href="/docker/runContainer/?image={{image.name}}&tag={$ imageTag['{{ image.name2 }}'] $}">{% trans "Create" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
.images-grid {
grid-template-columns: 1fr;
}
.header-actions {
flex-direction: column;
width: 100%;
}
.header-actions .btn-primary,
.header-actions .btn-secondary {
width: 100%;
}
}
</style>
<div class="modern-container" ng-controller="manageImages">
<div class="page-header">
<h1 class="page-title">
<div class="docker-icon">
<i class="fab fa-docker" style="color: #5b5fcf; font-size: 1.75rem;"></i>
</div>
{% trans "Docker Images" %}
</h1>
<p class="page-subtitle">{% trans "Select an image to create a new container" %}</p>
<div class="header-actions">
<a href="{% url 'manageImages' %}" class="btn-primary">
<i class="fas fa-search"></i>
{% trans "Search & Manage Images" %}
</a>
<a href="{% url 'listContainers' %}" class="btn-secondary">
<i class="fas fa-cube"></i>
{% trans "View Containers" %}
</a>
</div>
</div>
<!-- Local Images Section -->
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-hdd"></i>
{% trans "Available Images" %}
<span class="badge badge-count">{{ images|length }}</span>
</h2>
<img id="imageLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
</div>
<div class="card-body">
{% if images %}
<div class="images-grid">
{% for name, image in images.items %}
<div class="image-card">
<div class="image-header">
<div class="image-icon">
<i class="fab fa-docker"></i>
</div>
<div class="image-name">
<h3 class="image-title">{{ image.name }}</h3>
<p class="image-subtitle">{% trans "Docker Image" %}</p>
</div>
</div>
<div class="tag-selector">
<label class="tag-label">{% trans "Select Tag" %}</label>
<select class="tag-select" id="{{ forloop.counter }}" ng-model="imageTag['{{ image.name2 }}']">
<option value="" disabled selected>{% trans "Choose a tag..." %}</option>
{% for tag in image.tags %}
<option value="{{ tag }}">{{ tag }}</option>
{% endfor %}
</select>
</div>
<div class="image-actions">
<a class="action-btn create"
ng-href="/docker/runContainer/?image={{ image.name }}&tag={$ imageTag['{{ image.name2 }}'] $}">
<i class="fas fa-plus-circle"></i>
{% trans "Create Container" %}
</a>
<button class="action-btn details" onclick="alert('Image details coming soon!')">
<i class="fas fa-info-circle"></i>
{% trans "Details" %}
</button>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-icon">
<i class="far fa-images"></i>
</div>
<h3 class="empty-title">{% trans "No Images Found" %}</h3>
<p class="empty-text">{% trans "You don't have any Docker images installed yet." %}</p>
<a href="{% url 'manageImages' %}" class="btn-primary">
<i class="fas fa-search"></i>
{% trans "Search for Images" %}
</a>
</div>
{% endif %}
</div>
</div>
<!-- Quick Tips Section -->
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-lightbulb"></i>
{% trans "Quick Tips" %}
</h2>
</div>
<div class="card-body">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem;">
<div style="background: #f8f9ff; padding: 1rem; border-radius: 8px; border-left: 4px solid #5b5fcf;">
<h4 style="font-size: 0.875rem; font-weight: 600; color: #1e293b; margin-bottom: 0.5rem;">
<i class="fas fa-tag" style="color: #5b5fcf; margin-right: 0.5rem;"></i>
{% trans "About Tags" %}
</h4>
<p style="font-size: 0.8125rem; color: #64748b; margin: 0;">
{% trans "Tags represent different versions of an image. 'latest' is the most recent stable version." %}
</p>
</div>
<div style="background: #f8f9ff; padding: 1rem; border-radius: 8px; border-left: 4px solid #10b981;">
<h4 style="font-size: 0.875rem; font-weight: 600; color: #1e293b; margin-bottom: 0.5rem;">
<i class="fas fa-memory" style="color: #10b981; margin-right: 0.5rem;"></i>
{% trans "Container Resources" %}
</h4>
<p style="font-size: 0.8125rem; color: #64748b; margin: 0;">
{% trans "You can set memory limits and other resources when creating a container." %}
</p>
</div>
<div style="background: #f8f9ff; padding: 1rem; border-radius: 8px; border-left: 4px solid #f59e0b;">
<h4 style="font-size: 0.875rem; font-weight: 600; color: #1e293b; margin-bottom: 0.5rem;">
<i class="fas fa-network-wired" style="color: #f59e0b; margin-right: 0.5rem;"></i>
{% trans "Port Mapping" %}
</h4>
<p style="font-size: 0.8125rem; color: #64748b; margin: 0;">
{% trans "Configure port mappings to access services running inside containers." %}
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block footer_scripts %}
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
{% endblock %}

0
dockerManager/templates/dockerManager/index.html Executable file → Normal file
View File

0
dockerManager/templates/dockerManager/install.html Executable file → Normal file
View File

828
dockerManager/templates/dockerManager/listContainers.html Executable file → Normal file
View File

@@ -7,171 +7,699 @@
{% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} -->
<style>
.modern-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.page-header {
text-align: center;
margin-bottom: 3rem;
padding: 3rem 0;
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
border-radius: 20px;
animation: fadeInDown 0.5s ease-out;
position: relative;
overflow: hidden;
}
.page-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 70% 30%, rgba(91, 95, 207, 0.15) 0%, transparent 50%);
animation: rotate 30s linear infinite;
}
.page-title {
font-size: 3rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
position: relative;
z-index: 1;
}
.docker-icon {
width: 60px;
height: 60px;
background: white;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.page-subtitle {
font-size: 1.25rem;
color: #64748b;
margin-bottom: 1.5rem;
position: relative;
z-index: 1;
}
.header-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
position: relative;
z-index: 1;
}
.btn-primary {
background: #5b5fcf;
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-size: 0.875rem;
}
.btn-primary:hover {
background: #4547a9;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(91, 95, 207, 0.4);
color: white;
}
.btn-danger {
background: #ef4444;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.825rem;
margin-right: 0.5rem;
}
.btn-danger:hover {
background: #dc2626;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
.btn-info {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.825rem;
margin-right: 0.5rem;
}
.btn-info:hover {
background: #2563eb;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.btn-success {
background: #10b981;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.825rem;
margin-right: 0.5rem;
}
.btn-success:hover {
background: #059669;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
}
.main-card {
background: white;
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 10px 40px rgba(0,0,0,0.08);
border: 1px solid #e8e9ff;
overflow: hidden;
margin-bottom: 2rem;
animation: fadeInUp 0.5s ease-out;
}
.card-header {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
padding: 1.5rem 2rem;
border-bottom: 1px solid #e8e9ff;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.card-body {
padding: 2rem;
}
.containers-table {
width: 100%;
background: white;
border-radius: 12px;
overflow: hidden;
border: 1px solid #e8e9ff;
margin-bottom: 2rem;
}
.containers-table thead {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
}
.containers-table th {
padding: 1rem;
text-align: left;
font-weight: 600;
color: #1e293b;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 2px solid #e8e9ff;
}
.containers-table td {
padding: 1rem;
color: #64748b;
font-size: 0.875rem;
border-bottom: 1px solid #f3f4f6;
vertical-align: middle;
}
.containers-table tbody tr {
transition: all 0.2s ease;
}
.containers-table tbody tr:hover {
background: #f8f9ff;
}
.containers-table tbody tr:last-child td {
border-bottom: none;
}
.container-name {
display: flex;
align-items: center;
gap: 0.75rem;
}
.container-icon {
width: 32px;
height: 32px;
background: #e0e7ff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
color: #5b5fcf;
}
.status-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.status-running {
background: #d1fae5;
color: #065f46;
}
.status-stopped {
background: #fee2e2;
color: #991b1b;
}
.action-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 3px solid #e8e9ff;
border-top-color: #5b5fcf;
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
margin-left: 1rem;
}
.alert {
background: #fee2e2;
border: 1px solid #fecaca;
color: #991b1b;
padding: 1rem 1.5rem;
border-radius: 12px;
margin: 1rem 0;
}
.pagination {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 2rem;
}
.pagination li {
list-style: none;
}
.pagination li a {
display: block;
padding: 0.5rem 1rem;
background: #f8f9ff;
border: 1px solid #e8e9ff;
border-radius: 8px;
color: #5b5fcf;
text-decoration: none;
font-weight: 500;
transition: all 0.2s ease;
}
.pagination li a:hover {
background: #5b5fcf;
color: white;
}
.modal-content {
border-radius: 16px;
overflow: hidden;
border: none;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.modal-header {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
border-bottom: 1px solid #e8e9ff;
padding: 1.5rem 2rem;
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.modal-body {
padding: 2rem;
}
.modal-footer {
background: #f8f9ff;
border-top: 1px solid #e8e9ff;
padding: 1rem 2rem;
}
.modal-footer .btn {
padding: 0.5rem 1.5rem;
border-radius: 8px;
font-weight: 500;
font-size: 0.875rem;
transition: all 0.3s ease;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.modal-footer .btn.btn-primary {
background: #5b5fcf;
color: white;
}
.modal-footer .btn.btn-primary:hover {
background: #4547a9;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.3);
}
.modal-footer .btn.btn-secondary {
background: #6b7280;
color: white;
margin-left: 0.5rem;
}
.modal-footer .btn.btn-secondary:hover {
background: #4b5563;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(107, 114, 128, 0.3);
}
/* Fix button click issues */
.modal-footer .btn {
pointer-events: auto !important;
z-index: 1051 !important;
position: relative !important;
}
.modal {
z-index: 1050 !important;
}
.modal-backdrop {
z-index: 1040 !important;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.page-title {
font-size: 2rem;
}
.action-buttons {
flex-direction: column;
}
.action-buttons button {
width: 100%;
}
.containers-table {
font-size: 0.75rem;
}
.containers-table th,
.containers-table td {
padding: 0.5rem;
}
}
</style>
<div class="container">
<div id="page-title">
<h2 id="domainNamePage">{% trans "List Containers" %}
<a class="pull-right btn btn-primary" href="{% url "containerImage" %}">Create</a>
</h2>
<p>{% trans "Manage containers on server" %}</p>
</div>
<div class="panel">
<div class="panel-body">
<h3 class="content-box-header">
{% trans "Containers" %} <img id="imageLoading" src="/static/images/loading.gif" style="display: none;">
</h3>
<div ng-controller="listContainers" class="example-box-wrapper table-responsive">
<table cellpadding="0" cellspacing="0" border="0" class="table table-striped" id="datatable-example" style="padding:0px;">
<thead>
<tr>
<th>Name</th>
<th>Launch</th>
<th>Owner</th>
<th>Image</th>
<th>Tag</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="web in ContainerList track by $index">
<td ng-bind="web.name"></td>
<td><a href="/docker/view/{$ web.name $}"><img width="30px" height="30" class="" src="{% static 'baseTemplate/assets/image-resources/webPanel.png' %}"></a></td>
<td ng-bind="web.admin"></td>
<td ng-bind="web.image"></td>
<td ng-bind="web.tag"></td>
<td>
<button class="btn btn-primary" ng-click="delContainer(web.name)"><i class="fa fa-trash btn-icon"></i></button>
<button class="btn btn-primary" ng-click="showLog(web.name)"><i class="fa fa-file btn-icon"></i></button>
</td>
</tr>
</tbody>
</table>
<div id="listFail" class="alert alert-danger">
<p>{% trans "Error message:" %} {$ errorMessage $}</p>
<div class="modern-container" ng-controller="listContainers">
<div class="page-header">
<h1 class="page-title">
<div class="docker-icon">
<i class="fab fa-docker" style="color: #5b5fcf; font-size: 1.75rem;"></i>
</div>
{% trans "Container Management" %}
</h1>
<p class="page-subtitle">{% trans "Manage and monitor Docker containers on your server" %}</p>
<div class="header-actions">
<a href="{% url 'containerImage' %}" class="btn-primary">
<i class="fas fa-plus"></i>
{% trans "Create Container" %}
</a>
<a href="{% url 'manageImages' %}" class="btn-primary">
<i class="fas fa-hdd"></i>
{% trans "Manage Images" %}
</a>
</div>
<div class="row text-center">
<div class="col-sm-4 col-sm-offset-8">
<nav aria-label="Page navigation">
<ul class="pagination">
{% for items in pagination %}
<li ng-click="getFurtherContainersFromDB({{ forloop.counter }})" id="webPages"><a href="">{{ forloop.counter }}</a></li>
{% endfor %}
</ul>
</nav>
</div>
</div>
{% if showUnlistedContainer %}
<h3 class="title-hero">
{% trans "Unlisted Containers" %} <i class="fa fa-question-circle" title="{% trans "Containers listed below were either not created through panel or were not saved to database properly" %}"></i>
</h3>
<table cellpadding="0" cellspacing="0" border="0" class="table table-striped table-bordered" id="datatable-example">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for container in unlistedContainers %}
<tr>
<td>{{container.name}}</td>
<td>{{container.status}}</td>
<td>
<button class="btn btn-primary" ng-click="delContainer('{{container.name}}', true)"><i class="fa fa-trash"></i></button>
<button class="btn btn-primary" ng-click="showLog('{{container.name}}')"><i class="fa fa-file"></i></button>
<button class="btn btn-primary" ng-click="assignContainer('{{container.name}}')"><i class="fa fa-user"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<div id="logs" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Container logs</h4>
</div>
<div class="modal-body">
<textarea name="logs" class="form-control" id="" cols="30" rows="10">{$ logs $}</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="showLog('', true)">Refresh</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
<!-- Main Containers Section -->
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-cube"></i>
{% trans "Active Containers" %}
<span id="imageLoading" style="display: none;" class="loading-spinner"></span>
</h2>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="containers-table">
<thead>
<tr>
<th>{% trans "Container" %}</th>
<th>{% trans "Owner" %}</th>
<th>{% trans "Image" %}</th>
<th>{% trans "Tag" %}</th>
<th style="text-align: center;">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="web in ContainerList track by $index">
<td>
<div class="container-name">
<div class="container-icon">
<i class="fab fa-docker"></i>
</div>
<div>
<strong ng-bind="web.name"></strong>
</div>
</div>
</td>
<td ng-bind="web.admin"></td>
<td ng-bind="web.image"></td>
<td ng-bind="web.tag"></td>
<td style="text-align: center;">
<div class="action-buttons">
<a class="btn-success" href="/docker/view/{$ web.name $}" title="{% trans 'Manage Container' %}">
<i class="fas fa-cog"></i>
</a>
<button class="btn-info" ng-click="showLog(web.name)" title="{% trans 'View Logs' %}">
<i class="fas fa-file-alt"></i>
</button>
<button class="btn-danger" ng-click="delContainer(web.name)" title="{% trans 'Delete Container' %}">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div id="listFail" class="alert" ng-show="errorMessage">
<i class="fas fa-exclamation-triangle" style="margin-right: 0.5rem;"></i>
<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>
</div>
</div>
</div>
<!-- Unlisted Containers Section -->
{% if showUnlistedContainer %}
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-question-circle"></i>
{% trans "Unlisted Containers" %}
</h2>
</div>
<div class="card-body">
<p style="color: #64748b; margin-bottom: 1.5rem;">
<i class="fas fa-info-circle" style="margin-right: 0.5rem;"></i>
{% trans "Containers listed below were either not created through the panel or were not saved to database properly" %}
</p>
<div class="table-responsive">
<table class="containers-table">
<thead>
<tr>
<th>{% trans "Container" %}</th>
<th>{% trans "Status" %}</th>
<th style="text-align: center;">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for container in unlistedContainers %}
<tr>
<td>
<div class="container-name">
<div class="container-icon">
<i class="fab fa-docker"></i>
</div>
<div>
<strong>{{container.name}}</strong>
</div>
</div>
</td>
<td>
<span class="status-badge {% if container.status == 'running' %}status-running{% else %}status-stopped{% endif %}">
{{container.status}}
</span>
</td>
<td style="text-align: center;">
<div class="action-buttons">
<button class="btn-success" ng-click="assignContainer('{{container.name}}')" title="{% trans 'Assign to User' %}">
<i class="fas fa-user-plus"></i>
</button>
<button class="btn-info" ng-click="showLog('{{container.name}}')" title="{% trans 'View Logs' %}">
<i class="fas fa-file-alt"></i>
</button>
<button class="btn-danger" ng-click="delContainer('{{container.name}}', true)" title="{% trans 'Delete Container' %}">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- Container Logs Modal -->
<div id="logs" 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-file-alt" style="margin-right: 0.5rem;"></i>
{% trans "Container Logs" %}
</h4>
<button type="button" class="close" data-dismiss="modal"
style="font-size: 1.5rem; background: none; border: none;">&times;</button>
</div>
<div class="modal-body">
<textarea name="logs" class="form-control" style="font-family: monospace; height: 400px; resize: vertical;"
readonly>{$ logs $}</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="showLog('', true)">
<i class="fas fa-sync-alt"></i>
{% trans "Refresh" %}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<i class="fas fa-times"></i>
{% trans "Close" %}
</button>
</div>
</div>
</div>
</div>
<!-- Assign Container Modal -->
<div id="assign" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Assign Container to user</h4>
</div>
<div class="modal-body">
<form action="/" class="form-horizontal">
<div ng-hide="installationDetailsForm" class="form-group">
<label class="col-sm-3 control-label">{% trans "Select Owner" %}</label>
<div class="col-sm-6">
<select ng-model="dockerOwner" class="form-control">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<i class="fas fa-user-plus" style="margin-right: 0.5rem;"></i>
{% trans "Assign Container to User" %}
</h4>
<button type="button" class="close" data-dismiss="modal"
style="font-size: 1.5rem; background: none; border: none;">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label style="display: block; margin-bottom: 0.5rem; font-weight: 500; color: #1e293b;">
{% trans "Select Owner" %}
</label>
<select ng-model="dockerOwner" class="form-control"
style="width: 100%; padding: 0.75rem; border: 1px solid #e8e9ff; border-radius: 8px;">
{% for user in adminNames %}
<option>{{user}}</option>
{% endfor %}
</select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="submitAssignContainer()">Submit</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" ng-click="testScope()" style="margin-right: 0.5rem;">
Test Scope
</button>
<button type="button" class="btn btn-primary" ng-click="submitAssignContainer()">
<i class="fas fa-check"></i>
{% trans "Assign" %}
</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<i class="fas fa-times"></i>
{% trans "Cancel" %}
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block footer_scripts %}
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
{% endblock %}

759
dockerManager/templates/dockerManager/manageImages.html Executable file → Normal file
View File

@@ -7,144 +7,647 @@
{% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} -->
<style>
.modern-container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.page-header {
text-align: center;
margin-bottom: 3rem;
padding: 3rem 0;
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
border-radius: 20px;
animation: fadeInDown 0.5s ease-out;
position: relative;
overflow: hidden;
}
.page-header::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle at 70% 30%, rgba(91, 95, 207, 0.15) 0%, transparent 50%);
animation: rotate 30s linear infinite;
}
.page-title {
font-size: 3rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
position: relative;
z-index: 1;
}
.docker-icon {
width: 60px;
height: 60px;
background: white;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.page-subtitle {
font-size: 1.25rem;
color: #64748b;
margin-bottom: 1.5rem;
position: relative;
z-index: 1;
}
.header-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
position: relative;
z-index: 1;
}
.btn-primary {
background: #5b5fcf;
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-size: 0.875rem;
}
.btn-primary:hover {
background: #4547a9;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(91, 95, 207, 0.4);
color: white;
}
.btn-secondary {
background: #fff;
color: #5b5fcf;
border: 1px solid #e8e9ff;
padding: 0.75rem 2rem;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
font-size: 0.875rem;
}
.btn-secondary:hover {
background: #f8f9ff;
border-color: #5b5fcf;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.2);
}
.btn-warning {
background: #f59e0b;
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.btn-warning:hover {
background: #d97706;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(245, 158, 11, 0.4);
}
.btn-danger {
background: #ef4444;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.825rem;
}
.btn-danger:hover {
background: #dc2626;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
}
.btn-info {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.825rem;
}
.btn-info:hover {
background: #2563eb;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}
.main-card {
background: white;
border-radius: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 10px 40px rgba(0,0,0,0.08);
border: 1px solid #e8e9ff;
overflow: hidden;
margin-bottom: 2rem;
animation: fadeInUp 0.5s ease-out;
}
.card-header {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
padding: 1.5rem 2rem;
border-bottom: 1px solid #e8e9ff;
display: flex;
justify-content: space-between;
align-items: center;
}
.card-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.card-body {
padding: 2rem;
}
.search-section {
background: #f8f9ff;
border: 1px solid #e8e9ff;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #1e293b;
font-size: 0.875rem;
}
.form-control {
width: 100%;
padding: 0.875rem 1rem;
border: 1px solid #e8e9ff;
border-radius: 10px;
font-size: 0.875rem;
transition: all 0.3s ease;
background: #fff;
}
.form-control:focus {
outline: none;
border-color: #5b5fcf;
box-shadow: 0 0 0 3px rgba(91, 95, 207, 0.1);
}
.images-table {
width: 100%;
background: white;
border-radius: 12px;
overflow: hidden;
border: 1px solid #e8e9ff;
margin-bottom: 2rem;
}
.images-table thead {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
}
.images-table th {
padding: 1rem;
text-align: left;
font-weight: 600;
color: #1e293b;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 2px solid #e8e9ff;
}
.images-table td {
padding: 1rem;
color: #64748b;
font-size: 0.875rem;
border-bottom: 1px solid #f3f4f6;
}
.images-table tbody tr {
transition: all 0.2s ease;
}
.images-table tbody tr:hover {
background: #f8f9ff;
}
.images-table tbody tr:last-child td {
border-bottom: none;
}
.image-name {
display: flex;
align-items: center;
gap: 0.75rem;
}
.image-icon {
width: 32px;
height: 32px;
background: #e0e7ff;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
color: #5b5fcf;
}
.official-badge {
background: #d1fae5;
color: #065f46;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-left: 0.5rem;
}
.action-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 3px solid #e8e9ff;
border-top-color: #5b5fcf;
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
margin-left: 1rem;
}
.modal-content {
border-radius: 16px;
overflow: hidden;
border: none;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.modal-header {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f1ff 100%);
border-bottom: 1px solid #e8e9ff;
padding: 1.5rem 2rem;
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.modal-body {
padding: 2rem;
}
.modal-footer {
background: #f8f9ff;
border-top: 1px solid #e8e9ff;
padding: 1rem 2rem;
}
.history-table {
width: 100%;
background: #f8f9ff;
border-radius: 8px;
overflow: hidden;
}
.history-table th {
background: #e8e9ff;
padding: 0.75rem;
text-align: left;
font-weight: 600;
color: #1e293b;
font-size: 0.875rem;
}
.history-table td {
padding: 0.75rem;
color: #64748b;
font-size: 0.875rem;
border-bottom: 1px solid #e8e9ff;
word-break: break-all;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
.page-title {
font-size: 2rem;
}
.action-buttons {
flex-direction: column;
}
.action-buttons button {
width: 100%;
}
.images-table {
font-size: 0.75rem;
}
.images-table th,
.images-table td {
padding: 0.5rem;
}
}
</style>
<div class="container" ng-controller="manageImages">
<div id="page-title">
<h2 id="domainNamePage">{% trans "Manage Images" %}
<a class="pull-right btn btn-primary" href="{% url "containerImage" %}">Create Container</a>
</h2>
<p>{% trans "On this page you can manage docker images." %}</p>
</div>
<div id="history" class="modal fade" role="dialog">
<div class="modal-dialog" style="width:96%">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Image history</h4>
</div>
<div class="modal-body">
<table cellpadding="0" cellspacing="0" border="0" class="table table-responsive table-striped" id="datatable-example" style="padding:0px;">
<thead>
<tr>
<th>ID</th>
<th>CreatedBy</th>
<th>Created</th>
<th>Comment</th>
<th>Size</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="history in historyList track by $index">
<th style="word-break: break-all;" ng-bind="history.Id"></th>
<th style="word-break: break-all;" ng-bind="history.CreatedBy"></th>
<th ng-bind="history.Created"></th>
<th ng-bind="history.Comment"></th>
<th ng-bind="history.Size"></th>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-body">
<h3 class="content-box-header">
{% trans "Images" %}
{% trans "Images" %} <img id="imageLoading" src="/static/images/loading.gif" style="display: none;">
<button class="btn btn-warning pull-right" ng-click="rmImage(0)" title="{% trans 'Delete unused images' %}">Prune</button>
</h3><br>
<div class="row mx-10">
<label class="col-sm-2 control-label text-right">Search Image</label>
<div class="col-sm-6">
<input type="text" ng-change="searchImages()" ng-model="searchString" class="form-control">
<div class="modern-container" ng-controller="manageImages">
<div class="page-header">
<h1 class="page-title">
<div class="docker-icon">
<i class="fab fa-docker" style="color: #5b5fcf; font-size: 1.75rem;"></i>
</div>
<div class="col-sm-2"></div>
{% trans "Manage Docker Images" %}
</h1>
<p class="page-subtitle">{% trans "Pull, manage, and organize Docker images for your containers" %}</p>
<div class="header-actions">
<a href="{% url "containerImage" %}" class="btn-primary">
<i class="fas fa-plus"></i>
{% trans "Create Container" %}
</a>
</div>
<br>
</div>
<div class="example-box-wrapper table-responsive">
<table cellpadding="0" cellspacing="0" border="0" class="table table-striped " id="searchResult" style="padding:0px;">
<thead>
<tr>
<th>Name (search)</th>
<th>Tags</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="image in images track by $index">
<td>
<span ng-bind="image.name"></span>
<span ng-show="image.is_official == true"><i class="fa fa-check-circle btn-icon" title="{% trans 'Official image' %}"></i></span>
<!--<span><i class="fa fa-exclamation-circle" ng-attr-title="{$ image.description $}"></i></span>-->
</td>
<td>
<select ng-focus="loadTags($event)" ng-click="selectTag()" ng-model="imageTag[image.name2]" ng-options="tag for tag in tagList[image.name2]" ng-attr-id="{$ image.name2 $}" data-pageloaded='0' class="form-control ng-pristine ng-valid ng-empty ng-touched">
</select>
</td>
<td>
<a ng-click="pullImage(image.name, imageTag[image.name2])" class="btn btn-primary">{% trans "Pull" %}</a>
</td>
</tr>
</tbody>
<!-- Search Images Section -->
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-search"></i>
{% trans "Search & Pull Images" %}
</h2>
</div>
<div class="card-body">
<div class="search-section">
<div class="form-group">
<label class="form-label">
<i class="fas fa-search" style="margin-right: 0.5rem;"></i>
{% trans "Search Docker Hub" %}
</label>
<input type="text" ng-change="searchImages()" ng-model="searchString"
class="form-control" placeholder="{% trans 'Enter image name (e.g., nginx, mysql, ubuntu)' %}">
</div>
</div>
<div class="table-responsive">
<table class="images-table" id="searchResult">
<thead>
<tr>
<th>{% trans "Image Name" %}</th>
<th>{% trans "Tags" %}</th>
<th style="text-align: center;">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="image in images track by $index">
<td>
<div class="image-name">
<div class="image-icon">
<i class="fab fa-docker"></i>
</div>
<div>
<span ng-bind="image.name"></span>
<span ng-show="image.is_official == true" class="official-badge">
<i class="fas fa-check"></i>
{% trans "Official" %}
</span>
</div>
</div>
</td>
<td>
<select ng-focus="loadTags($event)" ng-click="selectTag()"
ng-model="imageTag[image.name2]"
ng-options="tag for tag in tagList[image.name2]"
ng-attr-id="{$ image.name2 $}"
data-pageloaded='0'
class="form-control">
<option value="">{% trans "Select tag..." %}</option>
</select>
</td>
<td style="text-align: center;">
<button ng-click="pullImage(image.name, imageTag[image.name2])"
class="btn-primary">
<i class="fas fa-download"></i>
{% trans "Pull" %}
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<table cellpadding="0" cellspacing="0" border="0" class="table table-striped " id="imageList" style="padding:0px;">
<thead>
<tr>
<th>Name (Locally Available)</th>
<th>Tags</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for name, image in images.items %}
<tr>
<td>{{image.name}}</td>
<td>
<select class="form-control tagList" id="{{forloop.counter}}">
{% for tag in image.tags%}
<option>{{tag}}</option>
{% endfor %}
</select>
</td>
<td>
<button class="btn btn-primary" title="History" ng-click="getHistory({{forloop.counter}})"><i class="fa fa-history btn-icon"></i></button>
<button class="btn btn-primary" title="Delete" ng-click="rmImage({{forloop.counter}})"><i class="fa fa-trash btn-icon"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
<!-- Local Images Section -->
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-hdd"></i>
{% trans "Local Images" %}
<span id="imageLoading" style="display: none;" class="loading-spinner"></span>
</h2>
<button class="btn-warning" ng-click="rmImage(0)" title="{% trans 'Delete unused images' %}">
<i class="fas fa-broom"></i>
{% trans "Prune Unused" %}
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="images-table" id="imageList">
<thead>
<tr>
<th>{% trans "Image Name" %}</th>
<th>{% trans "Tags" %}</th>
<th style="text-align: center;">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for name, image in images.items %}
<tr>
<td>
<div class="image-name">
<div class="image-icon">
<i class="fab fa-docker"></i>
</div>
<strong>{{ image.name }}</strong>
</div>
</td>
<td>
<select class="form-control tagList" id="{{ forloop.counter }}">
{% for tag in image.tags %}
<option>{{ tag }}</option>
{% endfor %}
</select>
</td>
<td style="text-align: center;">
<div class="action-buttons">
<button class="btn-info" title="{% trans 'View History' %}"
ng-click="getHistory({{ forloop.counter }})">
<i class="fas fa-history"></i>
</button>
<button class="btn-danger" title="{% trans 'Delete Image' %}"
ng-click="rmImage({{ forloop.counter }})">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- History Modal -->
<div id="history" 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-history" style="margin-right: 0.5rem;"></i>
{% trans "Image History" %}
</h4>
<button type="button" class="close" data-dismiss="modal"
style="font-size: 1.5rem; background: none; border: none;">&times;</button>
</div>
<div class="modal-body">
<table class="history-table">
<thead>
<tr>
<th>{% trans "ID" %}</th>
<th>{% trans "Created By" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Comment" %}</th>
<th>{% 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>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" data-dismiss="modal">
<i class="fas fa-times"></i>
{% trans "Close" %}
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block footer_scripts %}
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
{% endblock %}

1044
dockerManager/templates/dockerManager/runContainer.html Executable file → Normal file

File diff suppressed because it is too large Load Diff

1315
dockerManager/templates/dockerManager/viewContainer.html Executable file → Normal file

File diff suppressed because it is too large Load Diff

0
dockerManager/tests.py Executable file → Normal file
View File

2
dockerManager/urls.py Executable file → Normal file
View File

@@ -27,7 +27,7 @@ urlpatterns = [
re_path(r'^recreateContainer$', views.recreateContainer, name='recreateContainer'),
re_path(r'^installDocker$', views.installDocker, name='installDocker'),
re_path(r'^images$', views.images, name='containerImage'),
re_path(r'^view/(?P<n>.+)$', views.viewContainer, name='viewContainer'),
re_path(r'^view/(?P<name>.+)$', views.viewContainer, name='viewContainer'),
path('manage/<int:dockerapp>/app', Dockersitehome, name='Dockersitehome'),
path('getDockersiteList', views.getDockersiteList, name='getDockersiteList'),

4
dockerManager/views.py Executable file → Normal file
View File

@@ -99,6 +99,10 @@ def viewContainer(request, name):
except KeyError:
return redirect(loadLoginPage)
except Exception as e:
import traceback
error_msg = f"Error viewing container {name}: {str(e)}\n{traceback.format_exc()}"
return HttpResponse(error_msg, status=500)
@preDockerRun
def getTags(request):