From 184c123e230ba21946e9d8326987c74103bb021c Mon Sep 17 00:00:00 2001 From: ksyasuda Date: Sat, 17 Aug 2024 18:32:58 -0700 Subject: [PATCH] Initial commit --- .gitea/workflows/build-docker.yml | 43 +++++ .gitea/workflows/build-pypi.yml | 32 ++++ .gitignore | 6 + Dockerfile | 19 ++ LICENSE | 0 README.md | 0 docker-compose.yml | 10 + pyproject.toml | 32 ++++ requirements.txt | 49 +++++ src/__init__.py | 0 src/app.py | 104 +++++++++++ src/static/index.js | 238 ++++++++++++++++++++++++ src/static/ports.js | 82 ++++++++ src/static/style.css | 288 +++++++++++++++++++++++++++++ src/templates/image-port-list.html | 55 ++++++ src/templates/index.html | 67 +++++++ test.sh | 5 + version.txt | 1 + 18 files changed, 1031 insertions(+) create mode 100644 .gitea/workflows/build-docker.yml create mode 100644 .gitea/workflows/build-pypi.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/app.py create mode 100644 src/static/index.js create mode 100644 src/static/ports.js create mode 100644 src/static/style.css create mode 100644 src/templates/image-port-list.html create mode 100644 src/templates/index.html create mode 100755 test.sh create mode 100644 version.txt diff --git a/.gitea/workflows/build-docker.yml b/.gitea/workflows/build-docker.yml new file mode 100644 index 0000000..abedf7b --- /dev/null +++ b/.gitea/workflows/build-docker.yml @@ -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 \ No newline at end of file diff --git a/.gitea/workflows/build-pypi.yml b/.gitea/workflows/build-pypi.yml new file mode 100644 index 0000000..9c74dd2 --- /dev/null +++ b/.gitea/workflows/build-pypi.yml @@ -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/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bed67d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.vscode +env/* +__pycache__/* +dist/* +src/docker_apps_view.egg-info/* +src/__pycache__/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fa62051 --- /dev/null +++ b/Dockerfile @@ -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}"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8d1482d --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..94a3aa5 --- /dev/null +++ b/pyproject.toml @@ -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" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..676ae0a --- /dev/null +++ b/requirements.txt @@ -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 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..fdb5085 --- /dev/null +++ b/src/app.py @@ -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/", 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//stop", methods=["POST"]) +def stop_container(container_id): + container = client.containers.get(container_id) + container.stop() + return jsonify({"status": "stopped"}) + + +@app.route("/api/containers//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() diff --git a/src/static/index.js b/src/static/index.js new file mode 100644 index 0000000..7c5221a --- /dev/null +++ b/src/static/index.js @@ -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 = ` + ${container.name} + ${container.image} + + ${container.status} + + + ${container.host_ports.join(', ')} + ${container.docker_ports.join(', ')} + ${new Date(container.created).toLocaleString()} + ${container.network} + ${container.id} + + + + + `; + 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(); +}); \ No newline at end of file diff --git a/src/static/ports.js b/src/static/ports.js new file mode 100644 index 0000000..e167702 --- /dev/null +++ b/src/static/ports.js @@ -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); \ No newline at end of file diff --git a/src/static/style.css b/src/static/style.css new file mode 100644 index 0000000..b206705 --- /dev/null +++ b/src/static/style.css @@ -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 */ +} \ No newline at end of file diff --git a/src/templates/image-port-list.html b/src/templates/image-port-list.html new file mode 100644 index 0000000..0502854 --- /dev/null +++ b/src/templates/image-port-list.html @@ -0,0 +1,55 @@ + + + + + + Docker Image and Port List + + + + + + + + +
+

Docker Image and Port List

+ +
+ + + + + + + + + + + + + +
ImageHost PortDocker PortDocker NetworkCreated
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/src/templates/index.html b/src/templates/index.html new file mode 100644 index 0000000..ec092d7 --- /dev/null +++ b/src/templates/index.html @@ -0,0 +1,67 @@ + + + + + Docker Apps View + + + + + + + + +
+

Docker Apps View

+ +
+ + + + + + + + + + + + + + + + + + + + + +
NameImageStatusHost PortsDocker PortsCreatedNetworkIDActions
+
+ running + +
+
+
+ + +
+
+
+
+ + + + + + \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..248c6f6 --- /dev/null +++ b/test.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +. env/bin/activate + +FLASK_ENV=development FLASK_PORT=5000 python src/app.py \ No newline at end of file diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..8acdd82 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +0.0.1