Initial commit
This commit is contained in:
commit
184c123e23
43
.gitea/workflows/build-docker.yml
Normal file
43
.gitea/workflows/build-docker.yml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
name: Build Docker Image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- senpai
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Read current version
|
||||||
|
id: get_version
|
||||||
|
run: echo "current_version=$(cat VERSION)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Increment version
|
||||||
|
id: increment_version
|
||||||
|
run: |
|
||||||
|
current_version=${{ env.current_version }}
|
||||||
|
IFS='.' read -r -a version_parts <<< "$current_version"
|
||||||
|
version_parts[2]=$((version_parts[2] + 1))
|
||||||
|
new_version="${version_parts[0]}.${version_parts[1]}.${version_parts[2]}"
|
||||||
|
echo "new_version=$new_version" >> $GITHUB_ENV
|
||||||
|
echo $new_version > VERSION
|
||||||
|
|
||||||
|
- name: Log in to Gitea Docker Registry
|
||||||
|
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login https://gitea.suda.codes -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
gitea.suda.codes/sudacode/docker-apps-view:${{ env.new_version }}
|
||||||
|
gitea.suda.codes/sudacode/docker-apps-view:latest
|
||||||
|
|
||||||
|
- name: Log out from Gitea Docker Registry
|
||||||
|
run: docker logout https://gitea.suda.codes
|
32
.gitea/workflows/build-pypi.yml
Normal file
32
.gitea/workflows/build-pypi.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
name: Build and Upload Python Package
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- senpai
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Read version from file
|
||||||
|
id: read_version
|
||||||
|
run: echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install setuptools wheel twine build
|
||||||
|
|
||||||
|
- name: Build the package
|
||||||
|
run: python -m build
|
||||||
|
|
||||||
|
- name: Upload to Gitea PyPI Registry
|
||||||
|
run: |
|
||||||
|
twine upload --repository-url https://gitea.suda.codes/api/packages/sudacode/pypi -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} dist/*
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.vscode
|
||||||
|
env/*
|
||||||
|
__pycache__/*
|
||||||
|
dist/*
|
||||||
|
src/docker_apps_view.egg-info/*
|
||||||
|
src/__pycache__/*
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Use an official Python runtime as a parent image
|
||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
# Set the working directory in the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the current directory contents into the container at /app
|
||||||
|
COPY src /app
|
||||||
|
|
||||||
|
# Install any needed packages specified in requirements.txt
|
||||||
|
RUN pip install flask docker
|
||||||
|
|
||||||
|
ENV FLASK_PORT 3000
|
||||||
|
|
||||||
|
# Make port 5000 available to the world outside this container
|
||||||
|
EXPOSE ${FLASK_PORT}
|
||||||
|
|
||||||
|
# Run app.py when the container launches
|
||||||
|
CMD ["python", "app.py", "${FLASK_PORT}"]
|
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
docker_apps_view:
|
||||||
|
image: gitea.suda.codes/sudacode/docker-apps-view:latest
|
||||||
|
container_name: docker_apps_view
|
||||||
|
environment:
|
||||||
|
- FLASK_PORT=3000
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
restart: unless-stopped
|
32
pyproject.toml
Normal file
32
pyproject.toml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=42", "wheel", "setuptools_scm"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "docker_apps_view"
|
||||||
|
description = "Docker Apps View"
|
||||||
|
readme = "README.md"
|
||||||
|
authors = [
|
||||||
|
{ name = "sudacode", email = "suda@sudacode.com" }
|
||||||
|
]
|
||||||
|
license = { text = "MIT" }
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent"
|
||||||
|
]
|
||||||
|
requires-python = ">=3.6"
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"flask",
|
||||||
|
"pandas"
|
||||||
|
]
|
||||||
|
|
||||||
|
scripts = { docker_apps_view = "app:main" }
|
||||||
|
|
||||||
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
version_scheme = "guess-next-dev"
|
||||||
|
local_scheme = "node-and-date"
|
||||||
|
write_to = "version.txt"
|
49
requirements.txt
Normal file
49
requirements.txt
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
blinker==1.8.2
|
||||||
|
build==1.2.1
|
||||||
|
certifi==2024.7.4
|
||||||
|
cffi==1.17.0
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
click==8.1.7
|
||||||
|
cryptography==43.0.0
|
||||||
|
docker==7.1.0
|
||||||
|
-e git+ssh://git@gitea.suda.codes/sudacode/docker-apps-view.git@b3e65ffe95898741b8645a8941c046d43ebdce8e#egg=docker_apps_view
|
||||||
|
docutils==0.21.2
|
||||||
|
Flask==3.0.3
|
||||||
|
idna==3.7
|
||||||
|
importlib_metadata==8.2.0
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
jaraco.classes==3.4.0
|
||||||
|
jaraco.context==5.3.0
|
||||||
|
jaraco.functools==4.0.2
|
||||||
|
jeepney==0.8.0
|
||||||
|
Jinja2==3.1.4
|
||||||
|
keyring==25.3.0
|
||||||
|
markdown-it-py==3.0.0
|
||||||
|
MarkupSafe==2.1.5
|
||||||
|
mdurl==0.1.2
|
||||||
|
more-itertools==10.4.0
|
||||||
|
nh3==0.2.18
|
||||||
|
numpy==2.0.1
|
||||||
|
packaging==24.1
|
||||||
|
pandas==2.2.2
|
||||||
|
pkginfo==1.10.0
|
||||||
|
pycparser==2.22
|
||||||
|
Pygments==2.18.0
|
||||||
|
pyproject_hooks==1.1.0
|
||||||
|
python-dateutil==2.9.0.post0
|
||||||
|
pytz==2024.1
|
||||||
|
readme_renderer==44.0
|
||||||
|
requests==2.32.3
|
||||||
|
requests-toolbelt==1.0.0
|
||||||
|
rfc3986==2.0.0
|
||||||
|
rich==13.7.1
|
||||||
|
SecretStorage==3.3.3
|
||||||
|
setuptools==72.2.0
|
||||||
|
setuptools-scm==8.1.0
|
||||||
|
six==1.16.0
|
||||||
|
twine==5.1.1
|
||||||
|
tzdata==2024.1
|
||||||
|
urllib3==2.2.2
|
||||||
|
Werkzeug==3.0.3
|
||||||
|
wheel==0.44.0
|
||||||
|
zipp==3.20.0
|
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
104
src/app.py
Normal file
104
src/app.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
from os import getenv
|
||||||
|
|
||||||
|
import docker
|
||||||
|
from flask import Flask, jsonify, render_template, request
|
||||||
|
|
||||||
|
app = Flask(__name__, static_folder="static")
|
||||||
|
|
||||||
|
client = docker.from_env()
|
||||||
|
|
||||||
|
|
||||||
|
def get_containers_info():
|
||||||
|
containers = client.containers.list(all=True)
|
||||||
|
container_info = []
|
||||||
|
seen_ids = set()
|
||||||
|
|
||||||
|
for container in containers:
|
||||||
|
ports = container.attrs["NetworkSettings"]["Ports"]
|
||||||
|
|
||||||
|
for name, data in container.attrs["NetworkSettings"]["Networks"].items():
|
||||||
|
network = f"{name}: {data['IPAddress']}" if name != "host" else "host"
|
||||||
|
|
||||||
|
container_id = container.id[:12]
|
||||||
|
if container_id in seen_ids:
|
||||||
|
continue # Skip duplicate containers
|
||||||
|
seen_ids.add(container_id)
|
||||||
|
|
||||||
|
host_ports = []
|
||||||
|
docker_ports = []
|
||||||
|
for i in ports.items():
|
||||||
|
if isinstance(i[1], list):
|
||||||
|
docker_port, [host_binding, _] = i
|
||||||
|
else:
|
||||||
|
docker_port, host_binding = i
|
||||||
|
docker_ports.append(docker_port)
|
||||||
|
host_ports.append(
|
||||||
|
host_binding["HostPort"] if host_binding is not None else "None"
|
||||||
|
)
|
||||||
|
|
||||||
|
if host_ports == []:
|
||||||
|
host_ports = ["None"]
|
||||||
|
if docker_ports == []:
|
||||||
|
docker_ports = ["None"]
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"name": container.name,
|
||||||
|
"image": container.image.tags[0] if container.image.tags else "Unknown",
|
||||||
|
"status": container.status,
|
||||||
|
"docker_ports": docker_ports,
|
||||||
|
"host_ports": host_ports,
|
||||||
|
"created": container.attrs["Created"],
|
||||||
|
"id": container_id, # Shortened container ID
|
||||||
|
"network": network,
|
||||||
|
}
|
||||||
|
container_info.append(info)
|
||||||
|
|
||||||
|
return container_info
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/containers", methods=["GET"])
|
||||||
|
def containers():
|
||||||
|
containers = get_containers_info()
|
||||||
|
return jsonify(containers)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/containers/<container_id>", methods=["GET"])
|
||||||
|
def get_container_status(container_id):
|
||||||
|
container = client.containers.get(container_id)
|
||||||
|
container.reload()
|
||||||
|
return jsonify({"status": container.status})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/containers/<container_id>/stop", methods=["POST"])
|
||||||
|
def stop_container(container_id):
|
||||||
|
container = client.containers.get(container_id)
|
||||||
|
container.stop()
|
||||||
|
return jsonify({"status": "stopped"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/containers/<container_id>/restart", methods=["POST"])
|
||||||
|
def restart_container(container_id):
|
||||||
|
container = client.containers.get(container_id)
|
||||||
|
container.restart()
|
||||||
|
# Wait for the container to restart and get the updated status
|
||||||
|
container.reload()
|
||||||
|
return jsonify({"status": container.status})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/image-port-list")
|
||||||
|
def image_port_list():
|
||||||
|
return render_template("image-port-list.html")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
port = int(getenv("FLASK_PORT", 3000))
|
||||||
|
app.run(debug=False, host="0.0.0.0", port=port)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
238
src/static/index.js
Normal file
238
src/static/index.js
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
let containersData = [];
|
||||||
|
let dataTable;
|
||||||
|
|
||||||
|
async function fetchContainers() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/containers');
|
||||||
|
containersData = await response.json();
|
||||||
|
populateTable(containersData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching containers:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateTable(data) {
|
||||||
|
const tableBody = document.getElementById('containers-table-body');
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
data.forEach(container => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td>${container.name}</td>
|
||||||
|
<td>${container.image}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-text">${container.status}</span>
|
||||||
|
<span class="loading-icon" style="display: none;">🏃</span>
|
||||||
|
</td>
|
||||||
|
<td>${container.host_ports.join(', ')}</td>
|
||||||
|
<td>${container.docker_ports.join(', ')}</td>
|
||||||
|
<td>${new Date(container.created).toLocaleString()}</td>
|
||||||
|
<td>${container.network}</td>
|
||||||
|
<td>${container.id}</td>
|
||||||
|
<td class="action-buttons">
|
||||||
|
<button class="btn btn-warning btn-sm" onclick="stopContainer('${container.id}')">Stop</button>
|
||||||
|
<button class="btn btn-success btn-sm" onclick="restartContainer('${container.id}')">Restart</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize DataTables after populating the table
|
||||||
|
if ($.fn.DataTable.isDataTable('#dockerTable')) {
|
||||||
|
dataTable.clear().rows.add(data).draw();
|
||||||
|
} else {
|
||||||
|
dataTable = $('#dockerTable').DataTable({
|
||||||
|
pageLength: 25,
|
||||||
|
columnDefs: [
|
||||||
|
{
|
||||||
|
targets: [3, 4], // Host Port and Docker Port columns
|
||||||
|
type: 'port-sort'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targets: 6, // Network column
|
||||||
|
type: 'network-sort'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targets: 5, // Created column
|
||||||
|
type: 'date'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom sorting function for ports
|
||||||
|
$.fn.dataTable.ext.type.order['port-sort-pre'] = function(data) {
|
||||||
|
if (data) {
|
||||||
|
const firstPort = data.split(',')[0];
|
||||||
|
if (firstPort === 'None') {
|
||||||
|
return Number.MAX_SAFE_INTEGER; // Move 'None' to the end of the sort
|
||||||
|
}
|
||||||
|
const portNumber = firstPort.split('/')[0];
|
||||||
|
return parseInt(portNumber, 10);
|
||||||
|
}
|
||||||
|
return Number.MAX_SAFE_INTEGER; // Handle empty or malformed data
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom sorting function for network
|
||||||
|
$.fn.dataTable.ext.type.order['network-sort-pre'] = function(data) {
|
||||||
|
if (data === 'host') {
|
||||||
|
return Number.MAX_SAFE_INTEGER; // Move 'host' to the end of the sort
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
async function stopContainer(containerId) {
|
||||||
|
try {
|
||||||
|
const rows = dataTable.rows().nodes();
|
||||||
|
let row;
|
||||||
|
rows.each(function (r) {
|
||||||
|
if (r.cells[7].textContent === containerId) {
|
||||||
|
row = r;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('Container row not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = row.querySelector('.status-text');
|
||||||
|
const loadingIcon = row.querySelector('.loading-icon');
|
||||||
|
|
||||||
|
statusText.textContent = 'stopping';
|
||||||
|
loadingIcon.style.display = 'inline';
|
||||||
|
|
||||||
|
await fetch(`/api/containers/${containerId}/stop`, { method: 'POST' });
|
||||||
|
|
||||||
|
statusText.textContent = 'stopped';
|
||||||
|
loadingIcon.style.display = 'none';
|
||||||
|
dataTable.row(row).invalidate().draw(false); // Refresh the specific row
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping container:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restartContainer(containerId) {
|
||||||
|
try {
|
||||||
|
const rows = dataTable.rows().nodes();
|
||||||
|
let row;
|
||||||
|
rows.each(function (r) {
|
||||||
|
if (r.cells[7].textContent === containerId) {
|
||||||
|
row = r;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('Container row not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = row.querySelector('.status-text');
|
||||||
|
const loadingIcon = row.querySelector('.loading-icon');
|
||||||
|
|
||||||
|
statusText.textContent = 'restarting';
|
||||||
|
loadingIcon.style.display = 'inline';
|
||||||
|
|
||||||
|
await fetch(`/api/containers/${containerId}/restart`, { method: 'POST' });
|
||||||
|
|
||||||
|
// Poll the container status until it changes from "restarting"
|
||||||
|
const checkStatus = async () => {
|
||||||
|
const response = await fetch(`/api/containers/${containerId}`);
|
||||||
|
const container = await response.json();
|
||||||
|
if (container.status !== 'restarting') {
|
||||||
|
statusText.textContent = container.status;
|
||||||
|
loadingIcon.style.display = 'none';
|
||||||
|
dataTable.row(row).invalidate().draw(false); // Refresh the specific row
|
||||||
|
} else {
|
||||||
|
setTimeout(checkStatus, 500); // Check again after 1 second
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restarting container:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopContainer(containerId) {
|
||||||
|
try {
|
||||||
|
const rows = dataTable.rows().nodes();
|
||||||
|
let row;
|
||||||
|
rows.each(function (r) {
|
||||||
|
if (r.cells[7].textContent === containerId) {
|
||||||
|
row = r;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('Container row not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = row.querySelector('.status-text');
|
||||||
|
const loadingIcon = row.querySelector('.loading-icon');
|
||||||
|
|
||||||
|
statusText.textContent = 'stopping';
|
||||||
|
loadingIcon.style.display = 'inline';
|
||||||
|
|
||||||
|
await fetch(`/api/containers/${containerId}/stop`, { method: 'POST' });
|
||||||
|
|
||||||
|
statusText.textContent = 'stopped';
|
||||||
|
loadingIcon.style.display = 'none';
|
||||||
|
dataTable.row(row).invalidate().draw(false); // Refresh the specific row
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping container:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function restartContainer(containerId) {
|
||||||
|
try {
|
||||||
|
const rows = dataTable.rows().nodes();
|
||||||
|
let row;
|
||||||
|
rows.each(function (r) {
|
||||||
|
if (r.cells[7].textContent === containerId) {
|
||||||
|
row = r;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('Container row not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = row.querySelector('.status-text');
|
||||||
|
const loadingIcon = row.querySelector('.loading-icon');
|
||||||
|
|
||||||
|
statusText.textContent = 'restarting';
|
||||||
|
loadingIcon.style.display = 'inline';
|
||||||
|
|
||||||
|
await fetch(`/api/containers/${containerId}/restart`, { method: 'POST' });
|
||||||
|
|
||||||
|
// Poll the container status until it changes from "restarting"
|
||||||
|
const checkStatus = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/containers/${containerId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const container = await response.json();
|
||||||
|
if (container.status !== 'restarting') {
|
||||||
|
statusText.textContent = container.status;
|
||||||
|
loadingIcon.style.display = 'none';
|
||||||
|
dataTable.row(row).invalidate().draw(false); // Refresh the specific row
|
||||||
|
} else {
|
||||||
|
setTimeout(checkStatus, 500); // Check again after 500 milliseconds
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching container status:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the checkStatus function immediately
|
||||||
|
checkStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restarting container:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize components and fetch container data
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
fetchContainers();
|
||||||
|
});
|
82
src/static/ports.js
Normal file
82
src/static/ports.js
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
function populateTable(containersData) {
|
||||||
|
const tableBody = document.getElementById('image-port-table').getElementsByTagName('tbody')[0];
|
||||||
|
tableBody.innerHTML = ''; // Clear existing rows
|
||||||
|
|
||||||
|
containersData.forEach(container => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
const imageCell = document.createElement('td');
|
||||||
|
const hostPortCell = document.createElement('td');
|
||||||
|
const dockerPortCell = document.createElement('td');
|
||||||
|
const dockerNetworkCell = document.createElement('td');
|
||||||
|
const createdCell = document.createElement('td');
|
||||||
|
|
||||||
|
imageCell.textContent = container.image;
|
||||||
|
hostPortCell.textContent = container.host_ports;
|
||||||
|
dockerPortCell.textContent = container.docker_ports;
|
||||||
|
dockerNetworkCell.textContent = container.network;
|
||||||
|
createdCell.textContent = new Date(container.created).toLocaleString();
|
||||||
|
|
||||||
|
row.appendChild(imageCell);
|
||||||
|
row.appendChild(hostPortCell);
|
||||||
|
row.appendChild(dockerPortCell);
|
||||||
|
row.appendChild(dockerNetworkCell);
|
||||||
|
row.appendChild(createdCell);
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize DataTables with custom sorting for host and docker ports, network, and created columns
|
||||||
|
if ($.fn.DataTable.isDataTable('#image-port-table')) {
|
||||||
|
$('#image-port-table').DataTable().clear().rows.add(containersData).draw();
|
||||||
|
} else {
|
||||||
|
$('#image-port-table').DataTable({
|
||||||
|
pageLength: 25,
|
||||||
|
columnDefs: [
|
||||||
|
{
|
||||||
|
targets: [1, 2], // Host Port and Docker Port columns
|
||||||
|
type: 'port-sort'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targets: 3, // Network column
|
||||||
|
type: 'network-sort'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
targets: 4, // Created column
|
||||||
|
type: 'date'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom sorting function for ports
|
||||||
|
$.fn.dataTable.ext.type.order['port-sort-pre'] = function(data) {
|
||||||
|
if (data) {
|
||||||
|
const firstPort = data.split(',')[0];
|
||||||
|
if (firstPort === 'None') {
|
||||||
|
return Number.MAX_SAFE_INTEGER; // Move 'None' to the end of the sort
|
||||||
|
}
|
||||||
|
const portNumber = firstPort.split('/')[0];
|
||||||
|
return parseInt(portNumber, 10);
|
||||||
|
}
|
||||||
|
return Number.MAX_SAFE_INTEGER; // Handle empty or malformed data
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom sorting function for network
|
||||||
|
$.fn.dataTable.ext.type.order['network-sort-pre'] = function(data) {
|
||||||
|
if (data === 'host') {
|
||||||
|
return Number.MAX_SAFE_INTEGER; // Move 'host' to the end of the sort
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fetchContainers() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/containers');
|
||||||
|
const containersData = await response.json();
|
||||||
|
populateTable(containersData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching containers:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', fetchContainers);
|
288
src/static/style.css
Normal file
288
src/static/style.css
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
body {
|
||||||
|
background-color: #282a36;
|
||||||
|
color: #f8f8f2;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #bd93f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #44475a;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: #44475a;
|
||||||
|
color: #f8f8f2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #383a59;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(odd) {
|
||||||
|
background-color: #282a36;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #8be9fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Additional Bootstrap overrides or custom styles */
|
||||||
|
.table-striped tbody tr:nth-of-type(odd) {
|
||||||
|
background-color: #282a36;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-striped tbody tr:nth-of-type(even) {
|
||||||
|
background-color: #383a59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
background-color: #44475a;
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#service-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background-color: #282a36;
|
||||||
|
color: #f8f8f2;
|
||||||
|
border: 1px solid #44475a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: #bd93f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-text {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bubble {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-container {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom styles for DataTable search input and length selector with Dracula color scheme */
|
||||||
|
.dataTables_filter {
|
||||||
|
background-color: #282a36;
|
||||||
|
/* Dracula background color */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Light text foreground */
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_length {
|
||||||
|
background-color: #282a36;
|
||||||
|
/* Dracula background color */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Light text foreground */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_filter label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Light text foreground */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_filter input {
|
||||||
|
background-color: #44475a;
|
||||||
|
/* Dracula input background color */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Light text foreground */
|
||||||
|
border: 1px solid #6272a4;
|
||||||
|
/* Dracula border color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_filter input:focus {
|
||||||
|
background-color: #44475a;
|
||||||
|
/* Maintain Dracula input background color on focus */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Maintain light text foreground on focus */
|
||||||
|
border-color: #6272a4;
|
||||||
|
/* Maintain Dracula border color on focus */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_length select {
|
||||||
|
background-color: #44475a;
|
||||||
|
/* Dracula select background color */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Light text foreground */
|
||||||
|
border: 1px solid #6272a4;
|
||||||
|
/* Dracula border color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_length select:focus {
|
||||||
|
background-color: #44475a;
|
||||||
|
/* Maintain Dracula select background color on focus */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Maintain light text foreground on focus */
|
||||||
|
border-color: #6272a4;
|
||||||
|
/* Maintain Dracula border color on focus */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom styles for DataTable pagination with Dracula color scheme */
|
||||||
|
.dataTables_paginate {
|
||||||
|
background-color: #282a36;
|
||||||
|
/* Dracula background color */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Light text foreground */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_paginate .paginate_button {
|
||||||
|
background-color: #44475a;
|
||||||
|
/* Dracula button background color */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Light text foreground */
|
||||||
|
border: 1px solid #6272a4;
|
||||||
|
/* Dracula border color */
|
||||||
|
margin: 0 2px;
|
||||||
|
/* padding: 5px 10px; */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_paginate .paginate_button.current {
|
||||||
|
background-color: #50fa7b;
|
||||||
|
/* Dracula current page background color */
|
||||||
|
color: #282a36;
|
||||||
|
/* Dark text foreground for contrast */
|
||||||
|
border: 1px solid #6272a4;
|
||||||
|
/* Dracula border color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_paginate .paginate_button:focus {
|
||||||
|
background-color: #44475a;
|
||||||
|
/* Maintain Dracula button background color on focus */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Maintain light text foreground on focus */
|
||||||
|
border-color: #6272a4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
|
||||||
|
background-color: #ff79c6;
|
||||||
|
/* Dracula pink hover background */
|
||||||
|
border-color: #ff79c6;
|
||||||
|
/* Dracula pink hover border */
|
||||||
|
color: #282a36;
|
||||||
|
/* Dracula background for text on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Maintain Dracula border color on focus */
|
||||||
|
|
||||||
|
.page-link {
|
||||||
|
background-color: transparent;
|
||||||
|
/* Light text foreground */
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-link:hover {
|
||||||
|
background-color: #ff79c6;
|
||||||
|
/* Light text foreground */
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.disabled .page-link {
|
||||||
|
background-color: transparent;
|
||||||
|
/* Light text foreground */
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.disabled:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
/* Light text foreground */
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 5px;
|
||||||
|
/* Adjust the spacing as needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn {
|
||||||
|
background-color: #282a36;
|
||||||
|
/* Dracula background */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Dracula foreground */
|
||||||
|
border: 1px solid #6272a4;
|
||||||
|
/* Dracula border */
|
||||||
|
padding: 5px 10px;
|
||||||
|
/* Adjust the size as needed */
|
||||||
|
font-size: 0.9em;
|
||||||
|
/* Slightly smaller font size */
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn:hover {
|
||||||
|
background-color: #ff79c6;
|
||||||
|
/* Dracula pink hover background */
|
||||||
|
border-color: #ff79c6;
|
||||||
|
/* Dracula pink hover border */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dracula theming for the button */
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #282a36;
|
||||||
|
/* Dracula background */
|
||||||
|
color: #f8f8f2;
|
||||||
|
/* Dracula foreground */
|
||||||
|
border: 1px solid #6272a4;
|
||||||
|
/* Dracula border */
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #ff79c6;
|
||||||
|
/* Dracula pink hover background */
|
||||||
|
border-color: #ff79c6;
|
||||||
|
/* Dracula pink hover border */
|
||||||
|
color: #282a36;
|
||||||
|
/* Dracula background for text on hover */
|
||||||
|
}
|
55
src/templates/image-port-list.html
Normal file
55
src/templates/image-port-list.html
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Docker Image and Port List</title>
|
||||||
|
<!-- Import Bootstrap CSS -->
|
||||||
|
<link
|
||||||
|
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<!-- Import DataTables CSS -->
|
||||||
|
<link
|
||||||
|
href="https://cdn.datatables.net/1.10.21/css/dataTables.bootstrap4.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<!-- Link to external CSS file -->
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="text-center">Docker Image and Port List</h1>
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<a href="/" class="btn btn-primary">
|
||||||
|
← Back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-bordered text-center" id="image-port-table">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Image</th>
|
||||||
|
<th>Host Port</th>
|
||||||
|
<th>Docker Port</th>
|
||||||
|
<th>Docker Network</th>
|
||||||
|
<th>Created</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Rows will be populated by JavaScript -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Import jQuery -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
|
<!-- Import Bootstrap JS and dependencies -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.4/dist/umd/popper.min.js"></script>
|
||||||
|
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
||||||
|
<!-- Import DataTables JS -->
|
||||||
|
<script src="https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.10.21/js/dataTables.bootstrap4.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='ports.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
67
src/templates/index.html
Normal file
67
src/templates/index.html
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Docker Apps View</title>
|
||||||
|
<!-- Import Bootstrap CSS -->
|
||||||
|
<link
|
||||||
|
href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<!-- Import DataTables CSS -->
|
||||||
|
<link
|
||||||
|
href="https://cdn.datatables.net/1.10.21/css/dataTables.bootstrap4.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<!-- Link to external CSS file -->
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="text-center">Docker Apps View</h1>
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="{{ url_for('image_port_list') }}" class="btn btn-primary">
|
||||||
|
View Image and Port List
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="dockerTable" class="table table-striped table-bordered text-center">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Image</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col">Host Ports</th>
|
||||||
|
<th scope="col">Docker Ports</th>
|
||||||
|
<th scope="col">Created</th>
|
||||||
|
<th scope="col">Network</th>
|
||||||
|
<th scope="col">ID</th>
|
||||||
|
<th scope="col">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="containers-table-body">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="status-container">
|
||||||
|
<span class="status-text">running</span>
|
||||||
|
<img class="loading-icon" src="loading.gif" style="display: none;" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons" id="action-buttons">
|
||||||
|
<button class="btn btn-restart">Restart</button>
|
||||||
|
<button class="btn btn-stop">Stop</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Table rows will be populated by JavaScript -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script>
|
||||||
|
<script src="https://cdn.datatables.net/1.10.21/js/dataTables.bootstrap4.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='index.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
5
test.sh
Executable file
5
test.sh
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
. env/bin/activate
|
||||||
|
|
||||||
|
FLASK_ENV=development FLASK_PORT=5000 python src/app.py
|
1
version.txt
Normal file
1
version.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
0.0.1
|
Loading…
Reference in New Issue
Block a user