mirror of
https://github.com/usmannasir/cyberpanel.git
synced 2026-05-09 00:46:23 +02:00
design overall
This commit is contained in:
0
dockerManager/__init__.py
Executable file → Normal file
0
dockerManager/__init__.py
Executable file → Normal file
0
dockerManager/admin.py
Executable file → Normal file
0
dockerManager/admin.py
Executable file → Normal file
0
dockerManager/apps.py
Executable file → Normal file
0
dockerManager/apps.py
Executable file → Normal file
49
dockerManager/container.py
Executable file → Normal file
49
dockerManager/container.py
Executable file → Normal 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
0
dockerManager/decorators.py
Executable file → Normal file
0
dockerManager/dockerInstall.py
Executable file → Normal file
0
dockerManager/dockerInstall.py
Executable file → Normal file
0
dockerManager/migrations/__init__.py
Executable file → Normal file
0
dockerManager/migrations/__init__.py
Executable file → Normal file
0
dockerManager/models.py
Executable file → Normal file
0
dockerManager/models.py
Executable file → Normal file
0
dockerManager/pluginManager.py
Executable file → Normal file
0
dockerManager/pluginManager.py
Executable file → Normal file
0
dockerManager/signals.py
Executable file → Normal file
0
dockerManager/signals.py
Executable file → Normal file
142
dockerManager/static/dockerManager/dockerManager.js
Executable file → Normal file
142
dockerManager/static/dockerManager/dockerManager.js
Executable file → Normal 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
585
dockerManager/templates/dockerManager/images.html
Executable file → Normal 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
0
dockerManager/templates/dockerManager/index.html
Executable file → Normal file
0
dockerManager/templates/dockerManager/install.html
Executable file → Normal file
0
dockerManager/templates/dockerManager/install.html
Executable file → Normal file
828
dockerManager/templates/dockerManager/listContainers.html
Executable file → Normal file
828
dockerManager/templates/dockerManager/listContainers.html
Executable file → Normal 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">×</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;">×</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">×</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;">×</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
759
dockerManager/templates/dockerManager/manageImages.html
Executable file → Normal 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">×</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;">×</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
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
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
0
dockerManager/tests.py
Executable file → Normal file
2
dockerManager/urls.py
Executable file → Normal file
2
dockerManager/urls.py
Executable file → Normal 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
4
dockerManager/views.py
Executable file → Normal 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):
|
||||
|
||||
Reference in New Issue
Block a user