Initial commit
Some checks failed
Build Docker Image / build (push) Failing after 8s
Build and Upload Python Package / build (push) Failing after 10s

This commit is contained in:
ksyasuda 2024-08-17 18:32:58 -07:00
commit 184c123e23
18 changed files with 1031 additions and 0 deletions

View 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

View 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
View File

@ -0,0 +1,6 @@
.vscode
env/*
__pycache__/*
dist/*
src/docker_apps_view.egg-info/*
src/__pycache__/*

19
Dockerfile Normal file
View 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}"]

0
LICENSE Normal file
View File

0
README.md Normal file
View File

10
docker-compose.yml Normal file
View 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
View 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
View 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
View File

104
src/app.py Normal file
View 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
View 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;">&#x1F3C3;</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
View 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
View 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 */
}

View 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">
&larr; 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
View 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
View 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
View File

@ -0,0 +1 @@
0.0.1