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