Initial commit for v2.4.3

This commit is contained in:
usmannasir
2025-08-01 14:56:30 +05:00
commit 6dd7114f6d
4521 changed files with 1795978 additions and 0 deletions

BIN
dockerManager/.DS_Store vendored Normal file

Binary file not shown.

41
dockerManager/Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
# Use Debian as the base image
FROM debian:11.0
# Install required dependencies
RUN apt-get update && \
apt-get install -y wget gnupg ca-certificates
# Install OpenLiteSpeed
RUN wget -O - http://rpms.litespeedtech.com/debian/enable_lst_debian_repo.sh | bash
RUN apt-get install -y openlitespeed
# Install PHP and required PHP extensions for WordPress
RUN apt-get install -y lsphp82*
###
RUN rm -rf /usr/local/lsws/Example/html/*
# Install WP CLI
RUN wget https://github.com/wp-cli/wp-cli/releases/download/v2.7.1/wp-cli-2.7.1.phar
RUN chmod +x wp-cli-2.7.1.phar
RUN mv wp-cli-2.7.1.phar /usr/bin/wp
## set up vh conf
RUN rm -f /usr/local/lsws/conf/vhosts/Example/vhconf.conf
COPY ./vhconf.conf /usr/local/lsws/conf/vhosts/Example/vhconf.conf
# Expose necessary ports
EXPOSE 8088
# Copy entrypoint script
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
# Set execute permissions
RUN chmod +x /usr/local/bin/entrypoint.sh
# Define entrypoint
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View File

6
dockerManager/admin.py Normal file
View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
# Register your models here.

8
dockerManager/apps.py Normal file
View File

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from django.apps import AppConfig
class DockermanagerConfig(AppConfig):
name = 'dockerManager'

3
dockerManager/build.sh Normal file
View File

@@ -0,0 +1,3 @@
docker login
docker build -t cyberpanel/openlitespeed:latest .
docker push cyberpanel/openlitespeed:latest

1322
dockerManager/container.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,59 @@
from django.shortcuts import render,redirect
from loginSystem.models import Administrator
import os
import docker
import json
from django.http import HttpResponse
from loginSystem.views import loadLoginPage
from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter as logging
from plogical.acl import ACLManager
def preDockerRun(function):
def wrap(request, *args, **kwargs):
try:
userID = request.session['userID']
except KeyError:
return redirect(loadLoginPage)
currentACL = ACLManager.loadedACL(userID)
if request.method == "POST":
isPost = True
else:
isPost = False
# check if docker is installed
dockerInstallPath = '/usr/bin/docker'
if not os.path.exists(dockerInstallPath):
if isPost:
data_ret = {'status': 0, 'error_message': 'Docker not installed'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
else:
from plogical.httpProc import httpProc
proc = httpProc(request, 'dockerManager/install.html', {'status':currentACL['admin'], 'conErr':0}, 'admin')
return proc.render()
#return render(request, 'dockerManager/install.html', {'status':currentACL['admin'], 'conErr':0})
# Check if docker is running and we are able to connect
try:
client = docker.from_env()
result = client.ping()
except BaseException as msg:
logging.writeToFile(str(msg))
if isPost:
data_ret = {'status': 0, 'error_message': 'Docker daemon not running or not responsive'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
else:
from plogical.httpProc import httpProc
proc = httpProc(request, 'dockerManager/install.html', {'status': currentACL['admin'], 'conErr': 0},
'admin')
return proc.render()
#return render(request, 'dockerManager/install.html', {'status':currentACL['admin'], 'conErr':1})
return function(request, *args, **kwargs)
return wrap

View File

@@ -0,0 +1,29 @@
version: '3.8'
services:
wordpress:
image: cyberpanel/openlitespeed:latest
ports:
- "8000:8088"
# - "443:443"
environment:
DB_NAME: "wordpress"
DB_USER: "wpuser"
DB_PASSWORD: "wppassword"
WP_ADMIN_EMAIL: "admin@example.com"
WP_ADMIN_USER: "admin"
WP_ADMIN_PASSWORD: "adminpass"
WP_URL: docker.cyberpanel.net
DB_Host: mariadb:3306
SITE_NAME: "CyberPanel Docker Site"
depends_on:
- mariadb
mariadb:
image: mariadb
restart: always
environment:
ALLOW_EMPTY_PASSWORD=no
MYSQL_DATABASE: 'wordpress'
MYSQL_USER: 'wpuser'
MYSQL_PASSWORD: 'wppassword'
MYSQL_ROOT_PASSWORD: 'rootpassword'

View File

@@ -0,0 +1,100 @@
#!/usr/local/CyberCP/bin/python
import os
import sys
import time
sys.path.append('/usr/local/CyberCP')
import plogical.CyberCPLogFileWriter as logging
from serverStatus.serverStatusUtil import ServerStatusUtil
from plogical.processUtilities import ProcessUtilities
class DockerInstall:
@staticmethod
def submitInstallDocker(CommandCP=0):
try:
statusFile = open(ServerStatusUtil.lswsInstallStatusPath, 'w')
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Starting Docker Installation..\n", 1)
# Check if Podman is installed
if os.system("which podman > /dev/null 2>&1") == 0: #
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Podman detected. Removing Podman before installing Docker...\n", 1) #
# Stop and remove all Podman containers (NEW LINES)
ServerStatusUtil.executioner("podman stop -a", statusFile) #
ServerStatusUtil.executioner("podman rm -a", statusFile) #
# Remove Podman completely
if ProcessUtilities.decideDistro() in [ProcessUtilities.cent8, ProcessUtilities.centos]: #
ServerStatusUtil.executioner("dnf remove -y podman", statusFile) #
else: # Debian-based
ServerStatusUtil.executioner("DEBIAN_FRONTEND=noninteractive apt-get remove -y podman", statusFile) #
# Remove leftover Podman directories (NEW LINES)
ServerStatusUtil.executioner("rm -rf /var/lib/containers", statusFile) #
# Remove Podman socket if it exists (NEW LINES)
if os.path.exists("/run/podman/podman.sock"): #
os.remove("/run/podman/podman.sock") #
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Removed Podman socket.\n", 1) #
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Podman completely removed.\n", 1) #
# Check and unset DOCKER_HOST if set (NEW LINES)
if "DOCKER_HOST" in os.environ: #
ServerStatusUtil.executioner("unset DOCKER_HOST", statusFile) #
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Unset DOCKER_HOST to avoid conflicts.\n", 1) #
# Remove systemd override if it forces Podman for Docker (NEW LINES)
if os.path.exists("/etc/systemd/system/docker.service.d/override.conf"): #
os.remove("/etc/systemd/system/docker.service.d/override.conf") #
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Removed systemd override forcing Podman.\n", 1) #
# Install Docker based on OS version
if ProcessUtilities.decideDistro() == ProcessUtilities.cent8:
if os.path.exists(ProcessUtilities.debugPath):
logging.CyberCPLogFileWriter.writeToFile(f'Docker installation started for cent8/9')
commands = [
'sudo yum install -y yum-utils',
'yum install yum-utils -y',
'yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo',
'sudo dnf install docker-ce docker-ce-cli containerd.io docker-compose-plugin --allowerasing -y'
]
elif ProcessUtilities.decideDistro() == ProcessUtilities.centos:
commands = ['sudo yum install -y docker']
else:
commands = ['DEBIAN_FRONTEND=noninteractive apt-get install -y docker.io docker-compose']
for command in commands:
if CommandCP:
ProcessUtilities.executioner(command, 'root', True)
else:
if not ServerStatusUtil.executioner(command, statusFile):
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Failed to install Docker. [404]\n", 1)
return 0
# Enable and start Docker service
ProcessUtilities.executioner('sudo systemctl enable docker', 'root', True)
ProcessUtilities.executioner('sudo systemctl start docker', 'root', True)
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath,
"Docker successfully installed. [200]\n", 1)
time.sleep(2)
except BaseException as msg:
logging.CyberCPLogFileWriter.statusWriter(ServerStatusUtil.lswsInstallStatusPath, str(msg) + ' [404].', 1)
def main():
DockerInstall.submitInstallDocker()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Environment variables for WordPress installation
DB_NAME=${DB_NAME:-"wordpress"}
DB_USER=${DB_USER:-"wpuser"}
DB_PASSWORD=${DB_PASSWORD:-"wppassword"}
WP_ADMIN_EMAIL=${WP_ADMIN_EMAIL:-"admin@example.com"}
WP_ADMIN_USER=${WP_ADMIN_USER:-"admin"}
WP_ADMIN_PASSWORD=${WP_ADMIN_PASSWORD:-"adminpass"}
WP_URL=${WP_URL:-"docker.cyberpanel.net"}
DB_Host=${DB_Host:-"mariadb:3306"}
SITE_NAME=${SITE_NAME:-"CyberPanel Site"}
# Install WordPress using WP CLI
/usr/local/lsws/lsphp82/bin/php /usr/bin/wp core download --path=/usr/local/lsws/Example/html --allow-root
# Set up WP config
/usr/local/lsws/lsphp82/bin/php /usr/bin/wp core config --dbname="$DB_NAME" --dbuser="$DB_USER" --dbpass="$DB_PASSWORD" --path="/usr/local/lsws/Example/html" --dbhost="$DB_Host" --skip-check --allow-root
# Install WordPress
/usr/local/lsws/lsphp82/bin/php /usr/bin/wp core install --title="$SITE_NAME" --url="$WP_URL" --title="My WordPress Site" --admin_user="$WP_ADMIN_USER" --admin_password="$WP_ADMIN_PASSWORD" --admin_email="$WP_ADMIN_EMAIL" --path="/usr/local/lsws/Example/html" --skip-email --allow-root
### Install LSCache plugin
/usr/local/lsws/lsphp82/bin/php /usr/bin/wp plugin install litespeed-cache --allow-root --path="/usr/local/lsws/Example/html" --activate
# Start OpenLiteSpeed
/usr/local/lsws/bin/lswsctrl start
# Keep container running
tail -f /dev/null

View File

18
dockerManager/models.py Normal file
View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from django.db import models
from loginSystem.models import Administrator
# Create your models here.
class Containers(models.Model):
name = models.CharField(max_length=150,unique=True)
cid = models.CharField(max_length=64, default='')
admin = models.ForeignKey(Administrator, on_delete=models.CASCADE)
image = models.CharField(max_length=50, default='unknown')
tag = models.CharField(max_length=50, default='unknown')
memory = models.IntegerField(default=0)
ports = models.TextField(default="{}")
volumes = models.TextField(default="{}")
env = models.TextField(default="{}")
startOnReboot = models.IntegerField(default=0)

View File

@@ -0,0 +1,12 @@
from .signals import *
from plogical.pluginManagerGlobal import pluginManagerGlobal
class pluginManager:
@staticmethod
def preDockerInstallation(request):
return pluginManagerGlobal.globalPlug(request, preDockerInstallation)
@staticmethod
def postDockerInstallation(request, response):
return pluginManagerGlobal.globalPlug(request, postDockerInstallation, response)

10
dockerManager/signals.py Normal file
View File

@@ -0,0 +1,10 @@
# The world is a prison for the believer.
## https://www.youtube.com/watch?v=DWfNYztUM1U
from django.dispatch import Signal
## This event is fired before CyberPanel core start installation of Docker
preDockerInstallation = Signal()
## This event is fired after CyberPanel core finished intallation of Docker.
postDockerInstallation = Signal()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,556 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Docker Images - CyberPanel" %}{% endblock %}
{% block content %}
{% load static %}
{% 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);
}
.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;
}
.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 %}

View File

@@ -0,0 +1,67 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Docker Container Management - CyberPanel" %}{% endblock %}
{% block content %}
{% load static %}
{% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} -->
<div class="container">
<div id="page-title">
<h2>{% trans "Docker Container Management" %}</h2>
</div>
<div class="panel">
<div class="panel-body">
<h3 class="title-hero">
{% trans "Available Functions" %}
</h3>
<div class="example-box-wrapper">
<div class="row">
<div class="col-md-4">
<a href="{% url 'listContainers' %}" title="{% trans 'Manage Containers' %}" class="tile-box tile-box-shortcut btn-primary">
<div class="tile-header">
{% trans "Manage Containers" %}
</div>
<div class="tile-content-wrapper">
<i class="glyph-icon icon-dashboard"></i>
</div>
</a>
</div>
<div class="col-md-4">
<a href="{% url 'containerImage' %}" title="{% trans 'Create new container' %}" class="tile-box tile-box-shortcut btn-primary">
<div class="tile-header">
{% trans "New Container" %}
</div>
<div class="tile-content-wrapper">
<i class="glyph-icon icon-dashboard"></i>
</div>
</a>
</div>
<div class="col-md-4">
<a href="{% url 'manageImages' %}" title="{% trans 'Manage Images' %}" class="tile-box tile-box-shortcut btn-primary">
<div class="tile-header">
{% trans "Manage Images" %}
</div>
<div class="tile-content-wrapper">
<i class="glyph-icon icon-dashboard"></i>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,69 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Install Docker - CyberPanel" %}{% endblock %}
{% block content %}
{% load static %}
{% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} -->
<div class="container">
<div id="page-title">
<h2>{% trans "Install Docker" %}</h2>
</div>
<div ng-controller="installDocker" class="panel">
<div class="panel-body">
<h3 class="content-box-header">
{% trans "Install Docker" %} <img ng-hide="installDockerStatus"
src="{% static 'images/loading.gif' %}">
</h3>
<div class="example-box-wrapper text-center" style="white-space: normal;">
{% if conErr == 1 %}
{% if status == 1 %}
{% trans "Unable to connect to docker daemon, please try restarting docker from service page" %}
{% else %}
{% trans "You do not have sufficient permissions to access this page" %}
{% endif %}
{% else %}
<p class="h4">{% trans "Docker is currently not installed on this server. To manage containers, you must first install it." %}</p>
<!------ LSWS Switch box ----------------->
<div style="margin-top: 2%" ng-hide="installBoxGen" class="col-md-12">
<form action="/" id="" class="form-horizontal bordered-row panel-body">
<div class="form-group">
<div style="margin-top: 2%;" class="col-sm-12">
<textarea ng-model="requestData" rows="15"
class="form-control">{{ requestData }}</textarea>
</div>
</div>
</form>
</div>
<!----- LSWS Switch box ----------------->
<br>
{% if status == 1 %}
<button ng-hide="dockerInstallBTN" class="btn btn-primary" ng-click="installDocker()">Install Docker</button>
{% else %}
{% trans "You do not have permissions to install Docker. Please contact your system administrator" %}
{% endif %}
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}
{% block footer_scripts %}
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
{% endblock %}

View File

@@ -0,0 +1,705 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Containers List - CyberPanel" %}{% endblock %}
{% block content %}
{% load static %}
{% 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="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>
<!-- 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">
<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>
<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 %}

View File

@@ -0,0 +1,653 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Docker Manage Images - CyberPanel" %}{% endblock %}
{% block content %}
{% load static %}
{% 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="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 "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>
</div>
<!-- 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>
<!-- 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 %}

View File

@@ -0,0 +1,859 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{% trans "Create Container" %} - {{ image }}:{{ tag }}{% endblock %}
{% block content %}
{% load static %}
{% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} -->
<style>
.modern-container {
max-width: 1200px;
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;
}
.header-content {
position: relative;
z-index: 1;
}
.page-title {
font-size: 2.5rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.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);
}
.image-info {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
font-size: 1.125rem;
color: #64748b;
}
.image-badge {
background: #e0e7ff;
color: #5b5fcf;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.progress-indicator {
display: flex;
justify-content: center;
margin: 2rem 0;
gap: 1rem;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.progress-step::after {
content: '';
position: absolute;
top: 20px;
left: 60px;
width: 100%;
height: 2px;
background: #e8e9ff;
}
.progress-step:last-child::after {
display: none;
}
.step-icon {
width: 40px;
height: 40px;
background: #e8e9ff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
font-size: 1rem;
position: relative;
z-index: 1;
transition: all 0.3s ease;
}
.progress-step.active .step-icon {
background: #5b5fcf;
color: white;
box-shadow: 0 0 0 4px rgba(91, 95, 207, 0.2);
}
.progress-step.completed .step-icon {
background: #10b981;
color: white;
}
.step-label {
margin-top: 0.5rem;
font-size: 0.875rem;
color: #64748b;
font-weight: 500;
}
.progress-step.active .step-label {
color: #5b5fcf;
font-weight: 600;
}
.form-container {
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;
animation: fadeInUp 0.5s ease-out;
}
.form-section {
padding: 2rem;
border-bottom: 1px solid #e8e9ff;
}
.form-section:last-child {
border-bottom: none;
}
.section-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.section-icon {
width: 40px;
height: 40px;
background: #e0e7ff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #5b5fcf;
}
.section-title {
font-size: 1.125rem;
font-weight: 600;
color: #1e293b;
margin: 0;
}
.section-subtitle {
font-size: 0.875rem;
color: #64748b;
margin-top: 0.25rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1rem;
align-items: center;
margin-bottom: 1.5rem;
}
.form-label {
font-weight: 500;
color: #1e293b;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.required-star {
color: #ef4444;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #e8e9ff;
border-radius: 8px;
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);
}
.form-control:disabled {
background: #f8f9ff;
cursor: not-allowed;
color: #64748b;
}
.input-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.input-addon {
font-size: 0.875rem;
color: #64748b;
font-weight: 500;
}
.port-mapping {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 1rem;
align-items: center;
margin-bottom: 1rem;
padding: 1rem;
background: #f8f9ff;
border-radius: 8px;
border: 1px solid #e8e9ff;
}
.port-arrow {
color: #5b5fcf;
font-size: 1.25rem;
}
.port-label {
font-size: 0.75rem;
color: #64748b;
margin-bottom: 0.25rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.dynamic-section {
background: #f8f9ff;
border-radius: 12px;
padding: 1.5rem;
margin-top: 1.5rem;
}
.dynamic-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.dynamic-title {
font-size: 0.875rem;
font-weight: 600;
color: #1e293b;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.dynamic-item {
display: grid;
grid-template-columns: 1fr 2fr auto;
gap: 1rem;
align-items: center;
margin-bottom: 1rem;
padding: 1rem;
background: white;
border-radius: 8px;
border: 1px solid #e8e9ff;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.3s ease;
border: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: #5b5fcf;
color: white;
}
.btn-primary:hover {
background: #4547a9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.3);
}
.btn-secondary {
background: white;
color: #5b5fcf;
border: 1px solid #e8e9ff;
}
.btn-secondary:hover {
background: #f8f9ff;
border-color: #5b5fcf;
transform: translateY(-2px);
}
.btn-danger {
background: #fee2e2;
color: #ef4444;
border: 1px solid #fecaca;
}
.btn-danger:hover {
background: #fecaca;
transform: translateY(-2px);
}
.btn-success {
background: #d1fae5;
color: #065f46;
border: 1px solid #a7f3d0;
}
.btn-success:hover {
background: #a7f3d0;
transform: translateY(-2px);
}
.btn-lg {
padding: 1rem 2rem;
font-size: 1rem;
}
.action-buttons {
display: flex;
gap: 1rem;
margin-top: 2rem;
padding: 2rem;
background: #f8f9ff;
border-top: 1px solid #e8e9ff;
}
.status-message {
text-align: center;
padding: 2rem;
animation: fadeIn 0.5s ease-out;
}
.status-icon {
width: 60px;
height: 60px;
margin: 0 auto 1rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.status-success {
background: #d1fae5;
color: #065f46;
}
.status-error {
background: #fee2e2;
color: #991b1b;
}
.status-title {
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 0.5rem;
}
.status-text {
color: #64748b;
font-size: 0.875rem;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #e8e9ff;
border-top-color: #5b5fcf;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
.tooltip {
position: relative;
cursor: help;
}
.tooltip-icon {
color: #64748b;
font-size: 0.875rem;
}
.tooltip:hover .tooltip-content {
display: block;
}
.tooltip-content {
display: none;
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #1e293b;
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.75rem;
white-space: nowrap;
margin-bottom: 0.5rem;
}
.tooltip-content::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #1e293b;
}
@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);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@media (max-width: 768px) {
.form-row {
grid-template-columns: 1fr;
gap: 0.5rem;
}
.form-label {
margin-bottom: 0.25rem;
}
.port-mapping {
grid-template-columns: 1fr;
text-align: center;
}
.dynamic-item {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
.progress-indicator {
display: none;
}
}
</style>
<div class="modern-container" ng-controller="runContainer">
<div class="page-header">
<div class="header-content">
<h1 class="page-title">
<div class="docker-icon">
<i class="fab fa-docker" style="color: #5b5fcf; font-size: 1.75rem;"></i>
</div>
{% trans "Create New Container" %}
</h1>
<div class="image-info">
<span>{% trans "From image:" %}</span>
<span class="image-badge">
<i class="fas fa-cube"></i>
{{ image }}:{{ tag }}
</span>
</div>
</div>
</div>
<!-- Progress Indicator -->
<div class="progress-indicator" ng-hide="installationProgress">
<div class="progress-step active">
<div class="step-icon">
<i class="fas fa-cog"></i>
</div>
<span class="step-label">{% trans "Configure" %}</span>
</div>
<div class="progress-step">
<div class="step-icon">
<i class="fas fa-rocket"></i>
</div>
<span class="step-label">{% trans "Deploy" %}</span>
</div>
<div class="progress-step">
<div class="step-icon">
<i class="fas fa-check"></i>
</div>
<span class="step-label">{% trans "Complete" %}</span>
</div>
</div>
<!-- Main Form Container -->
<div class="form-container" ng-hide="installationDetailsForm">
<form name="dockerInstallForm" novalidate>
<!-- Basic Configuration Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">
<i class="fas fa-info-circle"></i>
</div>
<div>
<h2 class="section-title">{% trans "Basic Configuration" %}</h2>
<p class="section-subtitle">{% trans "Set the container name and resource limits" %}</p>
</div>
</div>
<div class="form-row">
<label class="form-label">
{% trans "Container Name" %}
<span class="required-star">*</span>
<div class="tooltip">
<i class="fas fa-question-circle tooltip-icon"></i>
<div class="tooltip-content">
{% trans "A unique name for your container" %}
</div>
</div>
</label>
<input type="text" class="form-control" ng-model="name" ng-init="name='{{ name }}'"
placeholder="{% trans 'e.g., my-nginx-server' %}" name="containerName" required>
</div>
<div class="form-row">
<label class="form-label">
{% trans "Owner" %}
<span class="required-star">*</span>
</label>
<select class="form-control" ng-model="dockerOwner" name="dockerOwner" required>
<option value="" disabled>{% trans "Select owner..." %}</option>
{% for owner in ownerList %}
<option value="{{ owner }}">{{ owner }}</option>
{% endfor %}
</select>
</div>
<div class="form-row">
<label class="form-label">
{% trans "Memory Limit" %}
<span class="required-star">*</span>
<div class="tooltip">
<i class="fas fa-question-circle tooltip-icon"></i>
<div class="tooltip-content">
{% trans "Maximum memory this container can use" %}
</div>
</div>
</label>
<div class="input-group">
<input type="number" class="form-control" ng-model="memory"
placeholder="512" min="128" name="memory" required>
<span class="input-addon">MB</span>
</div>
</div>
<div class="form-row">
<label class="form-label">{% trans "Image" %}</label>
<input type="text" class="form-control" ng-model="image" ng-init="image='{{ image }}'" disabled>
</div>
<div class="form-row">
<label class="form-label">{% trans "Tag" %}</label>
<input type="text" class="form-control" ng-model="tag" ng-init="tag='{{ tag }}'" disabled>
</div>
</div>
<!-- Port Configuration Section -->
{% if portConfig %}
<div class="form-section">
<div class="section-header">
<div class="section-icon">
<i class="fas fa-network-wired"></i>
</div>
<div>
<h2 class="section-title">{% trans "Port Mapping" %}</h2>
<p class="section-subtitle">{% trans "Map container ports to host ports for external access" %}</p>
</div>
</div>
{% for port, protocol in portConfig.items %}
<div class="port-mapping">
<div>
<div class="port-label">{% trans "Container Port" %}</div>
<div class="input-group">
<input type="text" class="form-control" value="{{ port }}" disabled>
<span class="input-addon">{{ protocol|upper }}</span>
</div>
</div>
<div class="port-arrow">
<i class="fas fa-arrow-right"></i>
</div>
<div>
<div class="port-label">{% trans "Host Port" %}</div>
<input type="number" class="form-control" ng-model="eport['{{ port }}']"
placeholder="{% trans 'e.g., 8080' %}" name="port_{{ port }}" required>
</div>
</div>
<span ng-init="iport[{{ port }}]={{ port }}; portType['{{ port }}']='{{ protocol }}'"></span>
{% endfor %}
</div>
{% endif %}
<!-- Environment Variables Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">
<i class="fas fa-list"></i>
</div>
<div>
<h2 class="section-title">{% trans "Environment Variables" %}</h2>
<p class="section-subtitle">{% trans "Configure environment variables for your container" %}</p>
</div>
</div>
<div class="dynamic-section">
<div class="dynamic-header">
<span class="dynamic-title">{% trans "Environment Variables" %}</span>
<button type="button" class="btn btn-secondary" ng-click="addEnvField()">
<i class="fas fa-plus"></i>
{% trans "Add Variable" %}
</button>
</div>
<span ng-init="envList = {}"></span>
{% for env, value in envList.items %}
<span ng-init="envList[{{ forloop.counter0 }}] = {'name':'{{ env }}', 'value':'{{ value }}'}"></span>
{% endfor %}
<div ng-repeat="env in envList track by $index" class="dynamic-item">
<input type="text" class="form-control" ng-model="envList[$index].name"
placeholder="{% trans 'Variable name' %}">
<input type="text" class="form-control" ng-model="envList[$index].value"
placeholder="{% trans 'Value' %}">
<button type="button" class="btn btn-danger" ng-click="removeEnvField($index)"
ng-show="Object.keys(envList).length > 0">
<i class="fas fa-trash"></i>
</button>
</div>
<div ng-show="Object.keys(envList).length === 0" style="text-align: center; padding: 2rem; color: #64748b;">
<i class="fas fa-info-circle" style="margin-right: 0.5rem;"></i>
{% trans "No environment variables configured. Click 'Add Variable' to add one." %}
</div>
</div>
</div>
<!-- Volume Mapping Section -->
<div class="form-section">
<div class="section-header">
<div class="section-icon">
<i class="fas fa-hdd"></i>
</div>
<div>
<h2 class="section-title">{% trans "Volume Mapping" %}</h2>
<p class="section-subtitle">{% trans "Mount host directories or volumes into the container" %}</p>
</div>
</div>
<div class="dynamic-section">
<div class="dynamic-header">
<span class="dynamic-title">{% trans "Volume Mappings" %}</span>
<button type="button" class="btn btn-secondary" ng-click="addVolField()">
<i class="fas fa-plus"></i>
{% trans "Add Volume" %}
</button>
</div>
<div ng-repeat="volume in volList track by $index" class="dynamic-item">
<input type="text" class="form-control" ng-model="volList[$index].src"
placeholder="{% trans 'Host path (e.g., /var/data)' %}">
<input type="text" class="form-control" ng-model="volList[$index].dest"
placeholder="{% trans 'Container path (e.g., /data)' %}">
<button type="button" class="btn btn-danger" ng-click="removeVolField()"
ng-show="$last && volListNumber > 0">
<i class="fas fa-trash"></i>
</button>
</div>
<div ng-show="volListNumber === 0" style="text-align: center; padding: 2rem; color: #64748b;">
<i class="fas fa-info-circle" style="margin-right: 0.5rem;"></i>
{% trans "No volumes configured. Click 'Add Volume' to mount directories." %}
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
{% verbatim %}
<button type="button" class="btn btn-primary btn-lg" ng-click="createContainer()"
ng-disabled="!formIsValid || containerCreationLoading">
<i class="fas fa-rocket"></i>
{% endverbatim %}
{% trans "Create Container" %}
{% verbatim %}
</button>
{% endverbatim %}
<a href="{% url 'containerImage' %}" class="btn btn-secondary btn-lg">
<i class="fas fa-arrow-left"></i>
{% trans "Cancel" %}
</a>
</div>
</form>
</div>
<!-- Progress/Status Section -->
<div class="form-container" ng-hide="installationProgress">
<div class="status-message">
<!-- Loading State -->
<div ng-show="containerCreationLoading">
<div class="loading-spinner"></div>
<h2 class="status-title" style="margin-top: 1.5rem;">{$ currentStatus $}</h2>
<p class="status-text">{% trans "Please wait while we create your container..." %}</p>
</div>
<!-- Success State -->
<div ng-hide="success">
<div class="status-icon status-success">
<i class="fas fa-check"></i>
</div>
<h2 class="status-title">{% trans "Container Created Successfully!" %}</h2>
<p class="status-text">{% trans "Your container has been created and is ready to use." %}</p>
<p class="status-text" style="margin-top: 1rem;">
<i class="fas fa-spinner fa-spin"></i>
{% trans "Redirecting to container management..." %}
</p>
</div>
<!-- Error State -->
<div ng-hide="errorMessageBox">
<div class="status-icon status-error">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h2 class="status-title">{% trans "Creation Failed" %}</h2>
<p class="status-text" style="color: #991b1b; margin-top: 1rem;">
<strong>{% trans "Error:" %}</strong> {$ errorMessage $}
</p>
<button type="button" class="btn btn-primary" ng-click="goBack()"
ng-disabled="goBackDisable" style="margin-top: 1.5rem;">
<i class="fas fa-arrow-left"></i>
{% trans "Go Back" %}
</button>
</div>
<!-- Connection Error -->
<div ng-hide="couldNotConnect">
<div class="status-icon status-error">
<i class="fas fa-wifi"></i>
</div>
<h2 class="status-title">{% trans "Connection Error" %}</h2>
<p class="status-text">{% trans "Could not connect to server. Please refresh this page and try again." %}</p>
<button type="button" class="btn btn-primary" onclick="location.reload()" style="margin-top: 1.5rem;">
<i class="fas fa-sync"></i>
{% trans "Refresh Page" %}
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block footer_scripts %}
<script src="{% static 'dockerManager/dockerManager.js' %}"></script>
<script>
// Initialize tooltips and other interactive elements
document.addEventListener('DOMContentLoaded', function() {
// Add any additional initialization code here
});
</script>
{% endblock %}

View File

@@ -0,0 +1,966 @@
{% extends "baseTemplate/index.html" %}
{% load i18n %}
{% block title %}{{ name }} - {% trans "Container Management" %}{% endblock %}
{% block content %}
{% load static %}
{% 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;
}
.container-header {
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
margin-bottom: 1rem;
position: relative;
z-index: 1;
}
.container-icon-large {
width: 80px;
height: 80px;
background: white;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
}
.container-title-section {
text-align: left;
}
.container-name {
font-size: 2.5rem;
font-weight: 700;
color: #1e293b;
margin: 0;
display: flex;
align-items: center;
gap: 1rem;
}
.container-id {
font-size: 0.875rem;
color: #64748b;
margin-top: 0.5rem;
font-family: monospace;
}
.status-badge {
padding: 0.5rem 1rem;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.5rem;
animation: pulse 2s infinite;
transition: all 0.3s ease;
}
.status-badge.status-changed {
animation: statusChange 0.6s ease-out;
}
@keyframes statusChange {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.status-running {
background: #d1fae5;
color: #065f46;
}
.status-stopped {
background: #fee2e2;
color: #991b1b;
}
.status-paused {
background: #fef3c7;
color: #92400e;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: blink 2s infinite;
}
.status-running .status-dot {
background: #10b981;
}
.status-stopped .status-dot {
background: #ef4444;
animation: none;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.refresh-icon {
cursor: pointer;
color: #64748b;
transition: all 0.3s ease;
}
.refresh-icon:hover {
color: #5b5fcf;
transform: rotate(180deg);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 10px 40px rgba(0,0,0,0.08);
border: 1px solid #e8e9ff;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.stat-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%;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 30px rgba(91, 95, 207, 0.15);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.stat-title {
font-size: 0.875rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stat-icon {
width: 40px;
height: 40px;
background: #e0e7ff;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #5b5fcf;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #1e293b;
margin: 0.5rem 0;
}
.stat-subtitle {
font-size: 0.875rem;
color: #64748b;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e8e9ff;
border-radius: 4px;
overflow: hidden;
margin-top: 1rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #5b5fcf 0%, #7b7fd0 100%);
border-radius: 4px;
transition: width 0.5s ease;
}
.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;
}
.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;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.info-item {
background: #f8f9ff;
padding: 1rem;
border-radius: 12px;
border: 1px solid #e8e9ff;
}
.info-label {
font-size: 0.75rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
.info-value {
font-size: 0.875rem;
font-weight: 500;
color: #1e293b;
word-break: break-all;
}
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1rem;
}
.action-btn {
background: #f8f9ff;
border: 1px solid #e8e9ff;
padding: 1rem;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
text-decoration: none;
color: #1e293b;
position: relative;
overflow: hidden;
}
.action-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
background: rgba(91, 95, 207, 0.1);
border-radius: 50%;
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.action-btn:hover::before {
width: 200px;
height: 200px;
}
.action-btn:hover {
transform: translateY(-2px);
border-color: #5b5fcf;
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.2);
}
.action-btn.primary {
background: #5b5fcf;
color: white;
border-color: #5b5fcf;
}
.action-btn.primary:hover {
background: #4547a9;
}
.action-btn.danger {
background: #fee2e2;
color: #ef4444;
border-color: #fecaca;
}
.action-btn.danger:hover {
background: #fecaca;
border-color: #ef4444;
}
.action-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.action-icon {
font-size: 1.5rem;
margin-bottom: 0.5rem;
display: block;
position: relative;
z-index: 1;
}
.action-text {
font-size: 0.875rem;
font-weight: 500;
position: relative;
z-index: 1;
}
.terminal-card {
background: #1a202c;
border-radius: 16px;
overflow: hidden;
margin-bottom: 2rem;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
.terminal-header {
background: #2d3748;
padding: 1rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #4a5568;
}
.terminal-title {
color: #e2e8f0;
font-size: 0.875rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
}
.terminal-controls {
display: flex;
gap: 0.5rem;
}
.terminal-btn {
background: #4a5568;
color: #e2e8f0;
border: 1px solid #718096;
padding: 0.375rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.75rem;
font-weight: 500;
}
.terminal-btn:hover {
background: #718096;
color: white;
}
.terminal-content {
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-size: 0.8125rem;
line-height: 1.5;
color: #e2e8f0;
background: #1a202c;
padding: 1.5rem;
height: 400px;
overflow-y: auto;
white-space: pre-wrap;
}
.terminal-content::-webkit-scrollbar {
width: 8px;
}
.terminal-content::-webkit-scrollbar-track {
background: #2d3748;
}
.terminal-content::-webkit-scrollbar-thumb {
background: #4a5568;
border-radius: 4px;
}
.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 Overrides */
.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;
}
.modal-footer .btn-primary {
background: #5b5fcf;
color: white;
}
.modal-footer .btn-primary:hover {
background: #4547a9;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(91, 95, 207, 0.3);
}
.modal-footer .btn-default {
background: #6b7280;
color: white;
margin-left: 0.5rem;
}
.modal-footer .btn-default:hover {
background: #4b5563;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(107, 114, 128, 0.3);
}
@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);
}
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(91, 95, 207, 0.4);
}
50% {
box-shadow: 0 0 0 10px rgba(91, 95, 207, 0);
}
}
@media (max-width: 768px) {
.container-header {
flex-direction: column;
text-align: center;
}
.container-title-section {
text-align: center;
}
.stats-grid {
grid-template-columns: 1fr;
}
.action-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
<div class="modern-container" ng-controller="viewContainer">
<div class="page-header">
<div class="container-header">
<div class="container-icon-large">
<i class="fab fa-docker" style="color: #5b5fcf; font-size: 2.5rem;"></i>
</div>
<div class="container-title-section">
<h1 class="container-name" ng-init="cName='{{ name }}';status='{{ status }}'">
{{ name }}
<span class="status-badge status-{{ status }}" ng-class="'status-' + status">
<span class="status-dot"></span>
<span ng-bind="status"></span>
</span>
</h1>
<div class="container-id">Container ID: {{ cid|slice:":12" }}...</div>
</div>
<i class="fas fa-sync refresh-icon" ng-click="refreshStatus()" title="{% trans 'Refresh status' %}"></i>
</div>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">{% trans "CPU Usage" %}</div>
<div class="stat-value" ng-init="cpuUsage={{ cpuUsage }}">{{ cpuUsage }}%</div>
<div class="stat-subtitle">{% trans "Processing Power" %}</div>
</div>
<div class="stat-icon">
<i class="fas fa-microchip"></i>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {{ cpuUsage }}%"></div>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">{% trans "Memory Usage" %}</div>
<div class="stat-value" ng-init="memoryUsage={{ memoryUsage }}">{{ memoryUsage|floatformat:"1" }}%</div>
<div class="stat-subtitle">{% trans "RAM Utilization" %}</div>
</div>
<div class="stat-icon">
<i class="fas fa-memory"></i>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: {{ memoryUsage }}%"></div>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<div>
<div class="stat-title">{% trans "Memory Limit" %}</div>
<div class="stat-value" ng-init="memory={{ memoryLimit }}">{{ memoryLimit }} MB</div>
<div class="stat-subtitle">{% trans "Allocated Memory" %}</div>
</div>
<div class="stat-icon">
<i class="fas fa-server"></i>
</div>
</div>
</div>
</div>
<!-- Container Information -->
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-info-circle"></i>
{% trans "Container Information" %}
</h2>
</div>
<div class="card-body">
<div class="info-grid">
<div class="info-item">
<div class="info-label">{% trans "Image" %}</div>
<div class="info-value">{{ image }}</div>
</div>
{% if ports %}
<div class="info-item">
<div class="info-label">{% trans "Port Mappings" %}</div>
<div class="info-value">
{% for iport, eport in ports.items %}
<span style="background: #e0e7ff; padding: 0.25rem 0.5rem; border-radius: 4px; margin-right: 0.5rem;">
{{ iport }} → {{ eport }}
</span>
{% endfor %}
</div>
</div>
{% endif %}
<div class="info-item">
<div class="info-label">{% trans "Restart Policy" %}</div>
<div class="info-value" ng-init="rPolicy='{{ restartPolicy }}'" ng-bind="rPolicy"></div>
</div>
<div class="info-item">
<div class="info-label">{% trans "Start on Boot" %}</div>
<div class="info-value" ng-init="startOnReboot={{ startOnReboot }}">
<span ng-if="startOnReboot" style="color: #10b981;">
<i class="fas fa-check-circle"></i> {% trans "Enabled" %}
</span>
<span ng-if="!startOnReboot" style="color: #64748b;">
<i class="fas fa-times-circle"></i> {% trans "Disabled" %}
</span>
</div>
</div>
</div>
</div>
</div>
<!-- Container Actions -->
<div class="main-card">
<div class="card-header">
<h2 class="card-title">
<i class="fas fa-cogs"></i>
{% trans "Container Actions" %}
<img id="actionLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
</h2>
</div>
<div class="card-body">
<div class="action-grid">
<div class="action-btn" ng-click="cAction('start')" ng-disabled="status=='running'">
<i class="fas fa-play action-icon" style="color: #10b981;"></i>
<div class="action-text">{% trans "Start" %}</div>
</div>
<div class="action-btn" ng-click="cAction('restart')" ng-disabled="status!='running'">
<i class="fas fa-sync-alt action-icon" style="color: #3b82f6;"></i>
<div class="action-text">{% trans "Restart" %}</div>
</div>
<div class="action-btn" ng-click="cAction('stop')" ng-disabled="status!='running'">
<i class="fas fa-stop action-icon" style="color: #f59e0b;"></i>
<div class="action-text">{% trans "Stop" %}</div>
</div>
<div class="action-btn" ng-click="cAction('pause')" ng-disabled="status!='running'">
<i class="fas fa-pause action-icon" style="color: #8b5cf6;"></i>
<div class="action-text">{% trans "Pause" %}</div>
</div>
<div class="action-btn danger" ng-click="cRemove()">
<i class="fas fa-trash action-icon"></i>
<div class="action-text">{% trans "Remove" %}</div>
</div>
<div class="action-btn" data-toggle="modal" data-target="#settings">
<i class="fas fa-sliders-h action-icon" style="color: #6366f1;"></i>
<div class="action-text">{% trans "Settings" %}</div>
</div>
<div class="action-btn" ng-click="recreate()">
<i class="fas fa-redo action-icon" style="color: #06b6d4;"></i>
<div class="action-text">{% trans "Recreate" %}</div>
</div>
<a href="/docker/exportContainer/?name={{ name }}" class="action-btn">
<i class="fas fa-download action-icon" style="color: #10b981;"></i>
<div class="action-text">{% trans "Export" %}</div>
</a>
<div class="action-btn" ng-click="showTop()" ng-disabled="loadingTop">
<i class="fas fa-terminal action-icon" style="color: #ec4899;"></i>
<div class="action-text">{% trans "Processes" %}</div>
</div>
</div>
</div>
</div>
<!-- Container Logs -->
<div class="terminal-card" ng-init="loadLogs('{{ name }}')">
<div class="terminal-header">
<h3 class="terminal-title">
<i class="fas fa-file-alt"></i>
{% trans "Container Logs" %}
</h3>
<div class="terminal-controls">
<button class="terminal-btn" ng-click="loadLogs('{{ name }}')">
<i class="fas fa-sync"></i> {% trans "Refresh" %}
</button>
<button class="terminal-btn" onclick="document.querySelector('.terminal-content').scrollTop = 0">
<i class="fas fa-arrow-up"></i> {% trans "Top" %}
</button>
<button class="terminal-btn" onclick="document.querySelector('.terminal-content').scrollTop = document.querySelector('.terminal-content').scrollHeight">
<i class="fas fa-arrow-down"></i> {% trans "Bottom" %}
</button>
</div>
</div>
<div class="terminal-content" ng-bind="logs"></div>
</div>
<!-- Settings Modal -->
<div id="settings" 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-cog" style="margin-right: 0.5rem;"></i>
{% trans "Container Settings" %}
</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">
<form name="containerSettingsForm" class="form-horizontal">
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Memory Limit (MB)" %}</label>
<div class="col-sm-6">
<input name="memory" type="number" class="form-control" ng-model="memory" required>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Start on Reboot" %}</label>
<div class="col-sm-9">
<div class="checkbox">
<label>
<input ng-model="startOnReboot" type="checkbox">
{% trans "Enable automatic startup" %}
</label>
</div>
</div>
</div>
<hr>
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Environment Variables" %}</label>
<div class="col-sm-9">
<div class="checkbox">
<label>
<input ng-model="envConfirmation" type="checkbox">
<strong>{% trans "I understand that editing ENV or Volumes will recreate the container" %}</strong>
</label>
</div>
</div>
</div>
<hr>
<!-- Environment Variables -->
<span ng-init="envList = {}"></span>
{% for env, value in envList.items %}
<span ng-init="envList[{{ forloop.counter0 }}] = {'name':'{{ env }}', 'value':'{{ value }}'}"></span>
{% endfor %}
<div ng-repeat="env in envList track by $index">
<div class="form-group">
<label class="col-sm-3 control-label" ng-show="$first">
{% trans "Environment Variables" %}
</label>
<label class="col-sm-3" ng-hide="$first"></label>
<div class="col-sm-3">
<input type="text" class="form-control" ng-disabled="!envConfirmation"
ng-model="envList[$index].name" placeholder="Variable name" required>
</div>
<div class="col-sm-4">
<input type="text" class="form-control" ng-disabled="!envConfirmation"
ng-model="envList[$index].value" placeholder="Value" required>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button type="button" class="btn btn-info" ng-disabled="!envConfirmation"
ng-click="addEnvField()">
<i class="fas fa-plus"></i> {% trans "Add Environment Variable" %}
</button>
</div>
</div>
<hr>
<!-- Volume Mappings -->
<span ng-init="volList = {}; volListNumber = 1"></span>
{% for key, value in volList.items %}
<span ng-init="volList[{{ forloop.counter0 }}] = {'dest':'{{ value.bind }}', 'src':'{{ key }}'}"></span>
<span ng-init="volListNumber = {{ forloop.counter0 }} + 1"></span>
{% endfor %}
<div class="form-group">
<label class="col-sm-3 control-label">{% trans "Volume Mappings" %}</label>
</div>
<div ng-repeat="volume in volList track by $index">
<div class="form-group">
<div class="col-sm-offset-3 col-sm-4">
<input type="text" class="form-control" ng-disabled="!envConfirmation"
ng-model="volList[$index].dest" placeholder="Container path" required>
</div>
<div class="col-sm-4">
<input type="text" class="form-control" ng-disabled="!envConfirmation"
ng-model="volList[$index].src" placeholder="Host path" required>
</div>
<div class="col-sm-1" ng-show="$last">
<button class="btn btn-danger" type="button" ng-disabled="!envConfirmation"
ng-click="removeVolField()">
<i class="fas fa-times"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<button type="button" class="btn btn-info" ng-disabled="!envConfirmation"
ng-click="addVolField()">
<i class="fas fa-plus"></i> {% trans "Add Volume Mapping" %}
</button>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<img id="containerSettingLoading" src="/static/images/loading.gif" style="display: none;" class="loading-spinner">
<button type="button" class="btn btn-primary" ng-disabled="savingSettings" ng-click="saveSettings()">
<i class="fas fa-save"></i> {% trans "Save Settings" %}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fas fa-times"></i> {% trans "Cancel" %}
</button>
</div>
</div>
</div>
</div>
<!-- Processes Modal -->
<div id="processes" 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-terminal" style="margin-right: 0.5rem;"></i>
{% trans "Container Processes" %}
</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="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th ng-repeat="item in topHead track by $index">{$ item $}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="process in topProcesses track by $index">
<td ng-repeat="item in process track by $index">{$ item $}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="showTop()">
<i class="fas fa-sync"></i> {% trans "Refresh" %}
</button>
<button type="button" class="btn btn-default" 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 %}

6
dockerManager/tests.py Normal file
View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from django.test import TestCase
# Create your tests here.

44
dockerManager/urls.py Normal file
View File

@@ -0,0 +1,44 @@
from django.urls import path, re_path
from . import views
from websiteFunctions.views import Dockersitehome, startContainer, stopContainer, restartContainer
urlpatterns = [
re_path(r'^$', views.loadDockerHome, name='dockerHome'),
# re_path(r'^images$', views.loadImages, name='loadImages'),
re_path(r'^getTags$', views.getTags, name='getTags'),
re_path(r'^runContainer', views.runContainer, name='runContainer'),
re_path(r'^submitContainerCreation$', views.submitContainerCreation, name='submitContainerCreation'),
re_path(r'^listContainers$', views.listContainers, name='listContainers'),
re_path(r'^getContainerList$', views.getContainerList, name='getContainerList'),
re_path(r'^getContainerLogs$', views.getContainerLogs, name='getContainerLogs'),
re_path(r'^installImage$', views.installImage, name='installImage'),
re_path(r'^delContainer$', views.delContainer, name='delContainer'),
re_path(r'^doContainerAction$', views.doContainerAction, name='doContainerAction'),
re_path(r'^getContainerStatus$', views.getContainerStatus, name='getContainerStatus'),
re_path(r'^exportContainer$', views.exportContainer, name='exportContainer'),
re_path(r'^saveContainerSettings$', views.saveContainerSettings, name='saveContainerSettings'),
re_path(r'^getContainerTop$', views.getContainerTop, name='getContainerTop'),
re_path(r'^assignContainer$', views.assignContainer, name='assignContainer'),
re_path(r'^searchImage$', views.searchImage, name='searchImage'),
re_path(r'^manageImages$', views.manageImages, name='manageImages'),
re_path(r'^getImageHistory$', views.getImageHistory, name='getImageHistory'),
re_path(r'^removeImage$', views.removeImage, name='removeImage'),
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<name>.+)$', views.viewContainer, name='viewContainer'),
path('manage/<int:dockerapp>/app', Dockersitehome, name='Dockersitehome'),
path('getDockersiteList', views.getDockersiteList, name='getDockersiteList'),
path('getContainerAppinfo', views.getContainerAppinfo, name='getContainerAppinfo'),
path('getContainerApplog', views.getContainerApplog, name='getContainerApplog'),
path('recreateappcontainer', views.recreateappcontainer, name='recreateappcontainer'),
path('RestartContainerAPP', views.RestartContainerAPP, name='RestartContainerAPP'),
path('StopContainerAPP', views.StopContainerAPP, name='StopContainerAPP'),
# Docker Container Actions
path('startContainer', startContainer, name='startContainer'),
path('stopContainer', stopContainer, name='stopContainer'),
path('restartContainer', restartContainer, name='restartContainer'),
]

100
dockerManager/vhconf.conf Normal file
View File

@@ -0,0 +1,100 @@
docRoot $VH_ROOT/html/
enableGzip 1
context / {
allowBrowse 1
location $DOC_ROOT/
rewrite {
RewriteFile .htaccess
}
}
expires {
enableExpires 1
}
index {
indexFiles index.php
autoIndex 0
useServer 0
}
errorPage 404{
url /error404.html
}
errorlog $VH_ROOT/logs/error.log{
logLevel DEBUG
rollingSize 10M
useServer 1
}
accessLog $VH_ROOT/logs/access.log{
compressArchive 0
logReferer 1
keepDays 30
rollingSize 10M
logUserAgent 1
useServer 0
}
rewrite {
enable 1
logLevel 0
}
hotlinkCtrl {
suffixes gif, jpeg, jpg
allowedHosts
allowDirectAccess 1
enableHotlinkCtrl 0
onlySelf 1
}
accessControl {
deny
allow *
}
realm SampleProtectedArea {
userDB {
cacheTimeout 60
maxCacheSize 200
location conf/vhosts/$VH_NAME/htpasswd
}
groupDB {
cacheTimeout 60
maxCacheSize 200
location conf/vhosts/$VH_NAME/htgroup
}
}
general {
enableContextAC 0
}
scripthandler {
add lsapi:cyberpanel php
}
extprocessor cyberpanel {
type lsapi
address UDS://tmp/lshttpd/cyberpanel.sock
maxConns 10
env LSAPI_CHILDREN=10
initTimeout 600
retryTimeout 0
persistConn 1
pcKeepAliveTimeout 1
respBuffer 0
autoStart 1
path /usr/local/lsws/lsphp82/bin/lsphp
# extUser nobody
# extGroup nobody
memSoftLimit 2047M
memHardLimit 2047M
procSoftLimit 400
procHardLimit 500
}

542
dockerManager/views.py Normal file
View File

@@ -0,0 +1,542 @@
# -*- coding: utf-8 -*-
from django.shortcuts import redirect, HttpResponse
from loginSystem.models import Administrator
from loginSystem.views import loadLoginPage
from plogical.DockerSites import Docker_Sites
from plogical.httpProc import httpProc
from .container import ContainerManager
from .decorators import preDockerRun
from plogical.acl import ACLManager
import json
# Create your views here.
# This function checks if user has admin permissions
def dockerPermission(request, userID, context):
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] != 1:
if request.method == "POST":
return ACLManager.loadErrorJson()
else:
return ACLManager.loadError()
else:
return 0
@preDockerRun
def loadDockerHome(request):
userID = request.session['userID']
admin = Administrator.objects.get(pk=userID)
template = 'dockerManager/index.html'
proc = httpProc(request, template, {"type": admin.type}, 'admin')
return proc.render()
def installDocker(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager(userID, 'submitInstallDocker')
cm.start()
data_ret = {'status': 1, 'error_message': 'None'}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
except BaseException as msg:
data_ret = {'status': 0, 'error_message': str(msg)}
json_data = json.dumps(data_ret)
return HttpResponse(json_data)
@preDockerRun
def installImage(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.submitInstallImage(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def viewContainer(request, name):
try:
if not request.GET._mutable:
request.GET._mutable = True
request.GET['name'] = name
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager(name)
coreResult = cm.loadContainerHome(request, userID)
return coreResult
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):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getTags(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def delContainer(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.submitContainerDeletion(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def recreateContainer(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.recreateContainer(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def runContainer(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
return cm.createContainer(request, userID)
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def listContainers(request):
try:
userID = request.session['userID']
cm = ContainerManager()
return cm.listContainers(request, userID)
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getContainerLogs(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getContainerLogs(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def submitContainerCreation(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.submitContainerCreation(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getContainerList(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
return cm.getContainerList(userID, json.loads(request.body))
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def doContainerAction(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.doContainerAction(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getContainerStatus(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getContainerStatus(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def exportContainer(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.exportContainer(request, userID)
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def saveContainerSettings(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.saveContainerSettings(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getContainerTop(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getContainerTop(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def assignContainer(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.assignContainer(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def searchImage(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.searchImage(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def images(request):
try:
userID = request.session['userID']
cm = ContainerManager()
coreResult = cm.images(request, userID)
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def manageImages(request):
try:
userID = request.session['userID']
cm = ContainerManager()
coreResult = cm.manageImages(request, userID)
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getImageHistory(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getImageHistory(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def removeImage(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.removeImage(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getDockersiteList(request):
import json
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getDockersiteList(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getContainerAppinfo(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getContainerAppinfo(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def getContainerApplog(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.getContainerApplog(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def recreateappcontainer(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.recreateappcontainer(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def RestartContainerAPP(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.RestartContainerAPP(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)
@preDockerRun
def StopContainerAPP(request):
try:
userID = request.session['userID']
currentACL = ACLManager.loadedACL(userID)
if currentACL['admin'] == 1:
pass
else:
return ACLManager.loadErrorJson()
cm = ContainerManager()
coreResult = cm.StopContainerAPP(userID, json.loads(request.body))
return coreResult
except KeyError:
return redirect(loadLoginPage)