Compare commits
115 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7d0bbc27fa | ||
![]() |
47c9ab68ce | ||
![]() |
518bc46dab | ||
![]() |
00216663a5 | ||
![]() |
f46648e598 | ||
![]() |
d905a95764 | ||
![]() |
a67f65996e | ||
![]() |
4b5ae32a97 | ||
![]() |
ee9870f554 | ||
![]() |
8f97e17765 | ||
![]() |
b4d472f414 | ||
![]() |
109d9bc171 | ||
![]() |
842f83ea91 | ||
![]() |
0dae02c382 | ||
![]() |
3080516d93 | ||
![]() |
928855d2f7 | ||
![]() |
29d630f1dd | ||
![]() |
a8bf38eedb | ||
![]() |
7cf8b84169 | ||
![]() |
83180e0a62 | ||
![]() |
869b7b30e7 | ||
![]() |
d0db603efe | ||
![]() |
774d5f4f62 | ||
![]() |
03be1187ef | ||
![]() |
57f080ec0d | ||
![]() |
04dbeefeb9 | ||
![]() |
d78cd645af | ||
![]() |
e1367b58f1 | ||
![]() |
37fd6f320d | ||
![]() |
6aa325ed8e | ||
![]() |
5081100f71 | ||
![]() |
cec389702c | ||
![]() |
82273d1fc1 | ||
![]() |
fbe26cd0ed | ||
![]() |
1558d61dcd | ||
![]() |
2d08e9fa3b | ||
![]() |
eda852b89e | ||
![]() |
c795cac009 | ||
![]() |
c71f330b49 | ||
![]() |
319aab60f5 | ||
![]() |
5581710e75 | ||
![]() |
82c134158d | ||
![]() |
2d9914c458 | ||
![]() |
cf41f07bbd | ||
![]() |
15722b1687 | ||
![]() |
62b7e73aac | ||
![]() |
64ec287286 | ||
![]() |
74cf69b3d3 | ||
![]() |
c9d7dea132 | ||
![]() |
42ca573b51 | ||
![]() |
9c41839852 | ||
![]() |
a73a89b250 | ||
![]() |
365cdde0cc | ||
![]() |
167dd8917e | ||
![]() |
8d9eb9981a | ||
![]() |
66f273e22e | ||
![]() |
6a352281ab | ||
![]() |
5c6e2a9eaa | ||
![]() |
8b8e30772f | ||
![]() |
7c5670e92b | ||
![]() |
c2f06639f5 | ||
![]() |
f04f08d44d | ||
![]() |
974d32e350 | ||
![]() |
b395de3445 | ||
![]() |
e78afb90ca | ||
![]() |
c9da3bd30b | ||
![]() |
bb84828ffe | ||
![]() |
12e75af9b0 | ||
![]() |
a841fed064 | ||
![]() |
308538f579 | ||
![]() |
f615a492e8 | ||
![]() |
6213c54165 | ||
![]() |
ff78e24913 | ||
![]() |
705779ec29 | ||
![]() |
c9c270fd81 | ||
![]() |
eb952c0a50 | ||
![]() |
7bf1739c52 | ||
![]() |
17a479be21 | ||
![]() |
7e3617f967 | ||
![]() |
785b54d5aa | ||
![]() |
2dc22fd75a | ||
![]() |
9a994bfbf1 | ||
![]() |
1d7b56907c | ||
![]() |
cfe9660ac2 | ||
![]() |
c7d79b296c | ||
![]() |
0596793c89 | ||
![]() |
575a689406 | ||
![]() |
d288cdb205 | ||
![]() |
f97628e9cd | ||
![]() |
32c2301873 | ||
![]() |
e294ca7089 | ||
![]() |
ea9ead5709 | ||
![]() |
eb992f706e | ||
![]() |
b62e209e6f | ||
![]() |
25280ae174 | ||
![]() |
c27f64f308 | ||
![]() |
a95b042960 | ||
![]() |
666f820a1f | ||
![]() |
97481b0b75 | ||
![]() |
04cc1c1df3 | ||
![]() |
003db6d7d1 | ||
![]() |
95dcedbdc1 | ||
![]() |
24941d5f32 | ||
![]() |
71bbb574d1 | ||
![]() |
13ee350bb2 | ||
![]() |
377ba6ae67 | ||
![]() |
e786b32161 | ||
![]() |
1938d7b2fc | ||
![]() |
70ec201924 | ||
![]() |
883a65faae | ||
![]() |
8feb88a2a0 | ||
![]() |
f94bd91898 | ||
![]() |
f058360b19 | ||
![]() |
5e45e084d0 | ||
![]() |
0f5575075e |
8
.dockerignore
Normal file
|
@ -0,0 +1,8 @@
|
|||
**/db.sqlite
|
||||
**/node_modules
|
||||
**/screenshots
|
||||
.gitignore
|
||||
.github
|
||||
.git
|
||||
Dockerfile
|
||||
docker-compose.yaml
|
2
.github/workflows/docker.yml
vendored
|
@ -55,7 +55,7 @@ jobs:
|
|||
|
||||
# Build image and only publish if not a Pull Request
|
||||
- name: Build and Publish Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
timeout-minutes: 30
|
||||
with:
|
||||
context: .
|
||||
|
|
11
.gitignore
vendored
|
@ -1,8 +1,5 @@
|
|||
**/node_modules/
|
||||
**/database.sqlite
|
||||
**/appdata/
|
||||
**/db.sqlite
|
||||
**/node_modules
|
||||
**/appdata
|
||||
.github
|
||||
test
|
||||
.dockerignore
|
||||
.gitignore
|
||||
docker-compose.yaml
|
||||
.git
|
52
CHANGELOG.md
|
@ -1,3 +1,55 @@
|
|||
## v0.60 (June 9th 2024) - Permissions system and import templates
|
||||
* Converted JS template literals into HTML.
|
||||
* Converted modals into HTML/HTMX.
|
||||
* Moved functions into dashboard controller.
|
||||
* New - Modal placeholder with loading spinner.
|
||||
* Container cards now update independently.
|
||||
* Container cards now display pending action (starting, stopping, pausing, restarting).
|
||||
* User avatars are now automatically generated.
|
||||
* Updated database models.
|
||||
* New - Multi-user permission system.
|
||||
* Refactored dashboard to support multiple users.
|
||||
* New - Banner alerts.
|
||||
* New - Template importing (*.yml, *.yaml, *.json).
|
||||
* Improved app search.
|
||||
* New - Search by category.
|
||||
* Updated dependencies.
|
||||
* Removed warning from the bottom of the registration page. Will be added back in a different location.
|
||||
* New - admin checks, session checks, and permission checks for router.
|
||||
* Added titles to activity indicators.
|
||||
* Created Github Wiki.
|
||||
* Added image pull to images page.
|
||||
* Images and volumes display 'In use'.
|
||||
* Images display tag.
|
||||
* Image pull gets latest if not set.
|
||||
* Updated buttons to trigger from 'mousedown' (John Carmack + Theo told me to).
|
||||
* Volumes page displays type (Volume or Bind).
|
||||
* Volume button is now functional.
|
||||
|
||||
## v0.40 (Feb 26th 2024) - HTMX rewrite
|
||||
* Pages rewritten to use HTMX.
|
||||
* Removed Socket.io.
|
||||
* Changed view files to *.HTML instead of *.EJS.
|
||||
* Removed "USER root" from Dockerfile.
|
||||
* Express sessions configured to use memorystore.
|
||||
* Improved chart rendering.
|
||||
* Improvements to container charts.
|
||||
* Created Variables page.
|
||||
* Created Supporters page.
|
||||
* Ability to remove images, volumes, or networks.
|
||||
* Fixed list.js sorting.
|
||||
* Fixed apps.js page navigation.
|
||||
* Removed stackfiles from templates.json and updated some icons.
|
||||
* New logo.
|
||||
* Improved handling of Docker events.
|
||||
* Improved dashboard responsiveness.
|
||||
* Updated server metrics styles.
|
||||
* Container cards display pending action.
|
||||
* Container charts only rendered if container running.
|
||||
* Created permissions modal.
|
||||
* Podman support (untested).
|
||||
* Started a new template for FOSS apps.
|
||||
|
||||
## v0.20 (Jan 20th 2024) - The rewrite. Jumping all the way to v0.20.
|
||||
* Changed to ES6 imports.
|
||||
* Cleaned up file structure and code layout.
|
||||
|
|
20
Dockerfile
|
@ -1,19 +1,7 @@
|
|||
FROM node:21-alpine
|
||||
|
||||
FROM node:22-alpine
|
||||
ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN --mount=type=bind,source=package.json,target=package.json \
|
||||
--mount=type=bind,source=package-lock.json,target=package-lock.json \
|
||||
--mount=type=cache,target=/root/.npm \
|
||||
npm ci --omit=dev
|
||||
|
||||
|
||||
USER root
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY . /app
|
||||
RUN npm install
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
CMD node server.js
|
89
README.md
|
@ -1,47 +1,39 @@
|
|||
# DweebUI
|
||||
DweebUI is a web interface for managing Docker, with a zero-config dashboard for controlling and monitoring your containers.
|
||||
|
||||
Alpha v0.20 ( :fire: Experimental :fire: )
|
||||
|
||||
|
||||
[:warning: DweebUI is a management interface and should not be directly exposed to the internet :warning:](https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet)
|
||||
|
||||
[](https://github.com/lllllllillllllillll)
|
||||
[](https://github.com/lllllllillllllillll)
|
||||
[](https://hub.docker.com/repository/docker/lllllllillllllillll/dweebui)
|
||||
[](https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE)
|
||||
[](https://www.buymeacoffee.com/lllllllillllllillll)
|
||||
|
||||
* This is a personal project I started to get more familiar with Javascript and Node.js.
|
||||
* Some UI elements are placeholders and every version may have breaking changes.
|
||||
* Please post issues and discussions so I know what bugs and features to focus on.
|
||||
|
||||
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard1.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard1.png" width="25%"/></a>
|
||||
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard2.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard2.png" width="25%"/></a>
|
||||
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/apps.png" width="25%"/></a>
|
||||
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/images.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/images.png" width="25%"/></a>
|
||||
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/register.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/register.png" width="25%"/></a>
|
||||
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/login.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/login.png" width="25%"/></a>
|
||||
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/syslogs.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/syslogs.png" width="25%"/></a>
|
||||
<a href="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/volumes.png"><img src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/volumes.png" width="25%"/></a>
|
||||
|
||||
|
||||
<h3 align="center"><img width="150" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/public/img/logo.png"></h3>
|
||||
<h4 align="center">DweebUI Beta v0.60 ( :fire: Experimental :fire: )</h4>
|
||||
<h3 align="center">Free and Open-Source WebUI For Managing Your Containers.</h3>
|
||||
<p align="center">
|
||||
<a href=""><img src="https://img.shields.io/github/stars/lllllllillllllillll/DweebUI?style=flat"/></a>
|
||||
<a href="https://github.com/lllllllillllllillll/DweebUI%2Fdev"><img src="https://img.shields.io/github/commit-activity/y/lllllllillllllillll/DweebUI%2Fdev"/></a>
|
||||
<a href="https://github.com/lllllllillllllillll/DweebUI%2Fdev"><img src="https://img.shields.io/github/last-commit/lllllllillllllillll/DweebUI%2Fdev"/></a>
|
||||
<a href="https://hub.docker.com/r/lllllllillllllillll/dweebui"><img src="https://img.shields.io/docker/pulls/lllllllillllllillll/dweebui"/></a>
|
||||
<a href="https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE"><img src="https://img.shields.io/github/license/lllllllillllllillll/DweebUI"/></a>
|
||||
<a href="https://www.reddit.com/r/dweebui"><img src="https://img.shields.io/badge/reddit-orange"/></a>
|
||||
<a href="https://www.buymeacoffee.com/lllllllillllllillll"><img src="https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee"/></a>
|
||||
</p>
|
||||
<h3 align="center"><img width="800" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI/main/screenshots/dashboard1.png"></h3>
|
||||
|
||||
## Features
|
||||
* [x] Dashboard provides server metrics, container metrics, and container controls, on a single page.
|
||||
* [x] View container logs.
|
||||
* [ ] Update containers (planned).
|
||||
* [ ] Manage your Docker networks, images, and volumes (in development).
|
||||
* [x] Light/Dark Mode.
|
||||
* [x] Easy to install app templates.
|
||||
* [x] Multi-User built-in.
|
||||
* [ ] Permissions system (in development).
|
||||
* [x] Support for Windows, Linux, and MacOS.
|
||||
* [ ] Docker compose import (in development).
|
||||
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
|
||||
* [x] Automatically persists data in docker volumes if bind mount isn't used.
|
||||
* [ ] Preset variables (planned).
|
||||
|
||||
* [x] A dynamically updating dashboard that displays server metrics along with container metrics and container controls.
|
||||
* [x] Multi-user support with permissions system.
|
||||
* [x] Container actions: Start, Stop, Pause, Restart, View Details, View Logs.
|
||||
* [x] Windows, Linux, and MacOS compatable.
|
||||
* [x] Light/Dark Mode.
|
||||
* [x] Mobile Friendly.
|
||||
* [x] Manage your Docker networks, images, and volumes.
|
||||
* [x] Easy to install app templates.
|
||||
* [x] Docker Compose Support.
|
||||
* [ ] Update containers (planned).
|
||||
* [x] Templates.json maintains compatability with Portainer, allowing you to use the template without needing to use DweebUI.
|
||||
* [ ] Preset variables (planned).
|
||||
* [ ] Themes (planned).
|
||||
|
||||
## About
|
||||
|
||||
* I started this as a personal project to get more familiar with Javascript and Node.js, so there may be some rough edges and spaghetti code.
|
||||
* I'm open to any contributions but you may want to wait until I reach v1.0 first.
|
||||
* Please post issues and discussions so I know what bugs and features to focus on.
|
||||
* DweebUI is a management interface and should not be directly exposed to the internet.
|
||||
|
||||
## Setup
|
||||
|
||||
|
@ -51,9 +43,8 @@ version: "3.9"
|
|||
services:
|
||||
dweebui:
|
||||
container_name: dweebui
|
||||
image: lllllllillllllillll/dweebui:v0.20
|
||||
image: lllllllillllllillll/dweebui
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 8000
|
||||
SECRET: MrWiskers
|
||||
restart: unless-stopped
|
||||
|
@ -61,7 +52,11 @@ services:
|
|||
- 8000:8000
|
||||
volumes:
|
||||
- dweebui:/app
|
||||
# Docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# Podman socket
|
||||
#- /run/podman/podman.sock:/var/run/docker.sock
|
||||
|
||||
networks:
|
||||
- dweebui_net
|
||||
|
||||
|
@ -73,13 +68,18 @@ networks:
|
|||
driver: bridge
|
||||
```
|
||||
|
||||
[Windows and MacOS Setup](https://github.com/lllllllillllllillll/DweebUI/wiki/Setup)
|
||||
|
||||
Compose setup:
|
||||
|
||||
* Paste the above content into a file named ```docker-compose.yml``` then place it in a folder named ```dweebui```.
|
||||
* Open a terminal in the ```dweebui``` folder, then enter ```docker compose up -d```.
|
||||
* You may need to use ```docker-compose up -d``` or execute the command as root with either ```sudo docker compose up -d``` or ```sudo docker-compose up -d```.
|
||||
|
||||
Configuration:
|
||||
|
||||
* `PORT` - Specifies which port the service binds to on startup. Default is `8000`.
|
||||
* `SECRET` - A shared secret used by the registration page.
|
||||
|
||||
## Credits
|
||||
|
||||
|
@ -91,4 +91,5 @@ Compose setup:
|
|||
|
||||
## Supporters
|
||||
|
||||
* MM (Patreon)
|
||||
* MM (Patreon)
|
||||
* PD (Buymeacoffee)
|
||||
|
|
|
@ -1,992 +0,0 @@
|
|||
export const appCard = (data) => {
|
||||
|
||||
|
||||
// dont look at anything in here.
|
||||
let app_name = data.name || data.title.toLowerCase();
|
||||
let shortened_name = "";
|
||||
let shortened_desc = data.description.slice(0, 60) + "...";
|
||||
let modal = app_name.replaceAll(" ", "-");
|
||||
let form_id = app_name.replaceAll("-", "_");
|
||||
let note = data.note ? data.note.replaceAll(". ", ".\n") : "no notes available";
|
||||
let description = data.description.replaceAll(". ", ".\n") || "no description available";
|
||||
let command = data.command ? data.command : "";
|
||||
let command_check = command ? "checked" : "";
|
||||
let privileged = data.privileged || "";
|
||||
let privileged_check = privileged ? "checked" : "";
|
||||
let repository = data.repository || "";
|
||||
let source = data.image || "";
|
||||
|
||||
let net_host, net_bridge, net_docker = '';
|
||||
let net_name = 'AppBridge';
|
||||
|
||||
// if data.network is set to host, bridge, or docker set the radio button to checked
|
||||
if (data.network == 'host') {
|
||||
net_host = 'checked';
|
||||
} else if (data.network) {
|
||||
net_bridge = 'checked';
|
||||
net_name = data.network;
|
||||
} else {
|
||||
net_docker = 'checked';
|
||||
}
|
||||
|
||||
|
||||
if (data.title.length > 28) {
|
||||
shortened_name = (data.title).slice(0, 25) + "...";
|
||||
}
|
||||
else {
|
||||
shortened_name = data.title;
|
||||
}
|
||||
|
||||
|
||||
if (repository != "") {
|
||||
source = (`${repository.url}/raw/master/${repository.stackfile}`);
|
||||
}
|
||||
|
||||
|
||||
function CatagoryColor(category) {
|
||||
switch (category) {
|
||||
case 'Other':
|
||||
return '<span class="badge bg-blue-lt">Other</span> ';
|
||||
case 'Productivity':
|
||||
return '<span class="badge bg-blue-lt">Productivity</span> ';
|
||||
case 'Tools':
|
||||
return '<span class="badge bg-blue-lt">Tools</span> ';
|
||||
case 'Dashboard':
|
||||
return '<span class="badge bg-blue-lt">Dashboard</span> ';
|
||||
case 'Communication':
|
||||
return '<span class="badge bg-azure-lt">Communication</span> ';
|
||||
case 'Media':
|
||||
return '<span class="badge bg-azure-lt">Media</span> ';
|
||||
case 'CMS':
|
||||
return '<span class="badge bg-azure-lt">CMS</span> ';
|
||||
case 'Monitoring':
|
||||
return '<span class="badge bg-indigo-lt">Monitoring</span> ';
|
||||
case 'LDAP':
|
||||
return '<span class="badge bg-purple-lt">LDAP</span> ';
|
||||
case 'Arr':
|
||||
return '<span class="badge bg-purple-lt">Arr</span> ';
|
||||
case 'Database':
|
||||
return '<span class="badge bg-red-lt">Database</span> ';
|
||||
case 'Paid':
|
||||
return '<span class="badge bg-red-lt" title="This is a paid product or contains paid features.">Paid</span> ';
|
||||
case 'Gaming':
|
||||
return '<span class="badge bg-pink-lt">Gaming</span> ';
|
||||
case 'Finance':
|
||||
return '<span class="badge bg-orange-lt">Finance</span> ';
|
||||
case 'Networking':
|
||||
return '<span class="badge bg-yellow-lt">Networking</span> ';
|
||||
case 'Authentication':
|
||||
return '<span class="badge bg-lime-lt">Authentication</span> ';
|
||||
case 'Development':
|
||||
return '<span class="badge bg-green-lt">Development</span> ';
|
||||
case 'Media Server':
|
||||
return '<span class="badge bg-teal-lt">Media Server</span> ';
|
||||
case 'Downloaders':
|
||||
return '<span class="badge bg-cyan-lt">Downloaders</span> ';
|
||||
default:
|
||||
return ''; // default to other if the category is not recognized
|
||||
}
|
||||
}
|
||||
|
||||
// set data.catagories to 'other' if data.catagories is empty or undefined
|
||||
if (data.categories == null || data.categories == undefined || data.categories == '') {
|
||||
data.categories = ['Other'];
|
||||
}
|
||||
|
||||
let categories = '';
|
||||
|
||||
for (let i = 0; i < data.categories.length; i++) {
|
||||
categories += CatagoryColor(data.categories[i]);
|
||||
}
|
||||
|
||||
if (data.restart_policy == null) {
|
||||
data.restart_policy = 'unless-stopped';
|
||||
}
|
||||
|
||||
let ports_data = [], volumes_data = [], env_data = [], label_data = [];
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
|
||||
// Get port details
|
||||
try {
|
||||
let ports = data.ports[i];
|
||||
let port_check = ports ? "checked" : "";
|
||||
let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
|
||||
let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
|
||||
let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
|
||||
|
||||
// remove /tcp or /udp from port_external if it exists
|
||||
if (port_external.includes("/")) {
|
||||
port_external = port_external.split("/")[0];
|
||||
}
|
||||
|
||||
ports_data.push({
|
||||
check: port_check,
|
||||
external: port_external,
|
||||
internal: port_internal,
|
||||
protocol: port_protocol
|
||||
});
|
||||
} catch {
|
||||
ports_data.push({
|
||||
check: "",
|
||||
external: "",
|
||||
internal: "",
|
||||
protocol: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get volume details
|
||||
try {
|
||||
let volumes = data.volumes[i];
|
||||
let volume_check = volumes ? "checked" : "";
|
||||
let volume_bind = volumes.bind ? volumes.bind : "";
|
||||
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
|
||||
let volume_readwrite = "rw";
|
||||
|
||||
if (volumes.readonly == true) {
|
||||
volume_readwrite = "ro";
|
||||
}
|
||||
|
||||
volumes_data.push({
|
||||
check: volume_check,
|
||||
bind: volume_bind,
|
||||
container: volume_container,
|
||||
readwrite: volume_readwrite
|
||||
});
|
||||
} catch {
|
||||
volumes_data.push({
|
||||
check: "",
|
||||
bind: "",
|
||||
container: "",
|
||||
readwrite: ""
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Get environment details
|
||||
try {
|
||||
let env = data.env[i];
|
||||
let env_check = "";
|
||||
let env_default = env.default ? env.default : "";
|
||||
if (env.set) { env_default = env.set;}
|
||||
let env_description = env.description ? env.description : "";
|
||||
let env_label = env.label ? env.label : "";
|
||||
let env_name = env.name ? env.name : "";
|
||||
|
||||
env_data.push({
|
||||
check: env_check,
|
||||
default: env_default,
|
||||
description: env_description,
|
||||
label: env_label,
|
||||
name: env_name
|
||||
});
|
||||
} catch {
|
||||
env_data.push({
|
||||
check: "",
|
||||
default: "",
|
||||
description: "",
|
||||
label: "",
|
||||
name: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get label details
|
||||
|
||||
try {
|
||||
let label = data.labels[i];
|
||||
let label_check = "";
|
||||
let label_name = label.name ? label.name : "";
|
||||
let label_value = label.value ? label.value : "";
|
||||
|
||||
label_data.push({
|
||||
check: label_check,
|
||||
name: label_name,
|
||||
value: label_value
|
||||
});
|
||||
} catch {
|
||||
label_data.push({
|
||||
check: "",
|
||||
name: "",
|
||||
value: ""
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return `
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 text-center">
|
||||
<span class="avatar avatar-xlplus mb-3 rounded"><img src='${data.logo}' width="144px" height="144px" loading="lazy"/></span>
|
||||
<h3 class="m-0 mb-1"><a href="#">${shortened_name}</a></h3>
|
||||
<div class="text-secondary">${shortened_desc}</div>
|
||||
<div class="mt-3">
|
||||
${categories}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-info">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-article" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path> <path d="M7 8h10"></path> <path d="M7 12h10"></path> <path d="M7 16h10"></path></svg>
|
||||
Learn More
|
||||
</a>
|
||||
<a href="#" class="card-btn" data-bs-toggle="modal" data-bs-target="#${modal}-install">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-bar-to-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M4 20l16 0"></path> <path d="M12 14l0 -10"></path> <path d="M12 14l4 -4"></path> <path d="M12 14l-4 -4"></path></svg>
|
||||
Install
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal modal-blur fade" id="${modal}-info" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="modal-title">${data.title}</div>
|
||||
<div>${description}</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal modal-blur fade" id="${modal}-install" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Install ${data.title}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<pre class="text-secondary">${note}</pre>
|
||||
|
||||
<form action="/install" name="${form_id}_install" id="${form_id}_install" method="POST">
|
||||
|
||||
<div class="row mb-3 align-items-end">
|
||||
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label">Container Name: </label>
|
||||
<input type="text" class="form-control" name="service_name" value="${app_name}" hidden/>
|
||||
<input type="text" class="form-control" name="name" value="${app_name}"/>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label">Image: </label>
|
||||
<input type="text" class="form-control" name="image" value="${source}"/>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label">Restart Policy: </label>
|
||||
<select class="form-select" name="restart_policy">
|
||||
<option value="${data.restart_policy}" selected hidden>${data.restart_policy}</option>
|
||||
<option value="unless-stopped">unless-stopped</option>
|
||||
<option value="on-failure">on-failure</option>
|
||||
<option value="never">never</option>
|
||||
<option value="always">always</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="form-label">Network Mode</label>
|
||||
<div class="form-selectgroup-boxes row mb-3">
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="net_mode" value="host" class="form-selectgroup-input" ${net_host}>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Host Network</span>
|
||||
<span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="net_mode" value="${net_name}" class="form-selectgroup-input" ${net_bridge}>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
|
||||
<span class="d-block text-secondary">Containers can communicate using names.</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="net_mode" value="docker" class="form-selectgroup-input" ${net_docker}>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
|
||||
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="accordion" id="${modal}-accordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-1">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1" aria-expanded="false">
|
||||
Ports
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port0" type="checkbox" ${ports_data[0].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">External Port</label>
|
||||
<input type="text" class="form-control" name="port_0_external" value="${ports_data[0].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Internal Port</label>
|
||||
<input type="text" class="form-control" name="port_0_internal" value="${ports_data[0].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">Protocol</label>
|
||||
<select class="form-select" name="port_0_protocol">
|
||||
<option value="${ports_data[0].protocol}" selected hidden>${ports_data[0].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port1" type="checkbox" ${ports_data[1].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_1_external" value="${ports_data[1].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_1_internal" value="${ports_data[1].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_1_protocol">
|
||||
<option value="${ports_data[1].protocol}" selected hidden>${ports_data[1].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port2" type="checkbox" ${ports_data[2].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_2_external" value="${ports_data[2].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_2_internal" value="${ports_data[2].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_2_protocol">
|
||||
<option value="${ports_data[2].protocol}" selected hidden>${ports_data[2].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port3" type="checkbox" ${ports_data[3].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_3_external" value="${ports_data[3].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_3_internal" value="${ports_data[3].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_3_protocol">
|
||||
<option value="${ports_data[3].protocol}" selected hidden>${ports_data[3].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port4" type="checkbox" ${ports_data[4].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_4_external" value="${ports_data[4].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_4_internal" value="${ports_data[4].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_4_protocol">
|
||||
<option value="${ports_data[4].protocol}" selected hidden>${ports_data[4].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port5" type="checkbox" ${ports_data[5].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_5_external" value="${ports_data[5].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_5_internal" value="${ports_data[5].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_5_protocol">
|
||||
<option value="${ports_data[5].protocol}" selected hidden>${ports_data[5].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-2">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-2" aria-expanded="false">
|
||||
Volumes
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-2" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume0" type="checkbox" ${volumes_data[0].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_0_bind" value="${volumes_data[0].bind}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_0_container" value="${volumes_data[0].container}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_0_readwrite">
|
||||
<option value="${volumes_data[0].readwrite}" selected hidden>${volumes_data[0].readwrite}</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume1" type="checkbox" ${volumes_data[1].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_1_bind" value="${volumes_data[1].bind}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_1_container" value="${volumes_data[1].container}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_1_readwrite">
|
||||
<option value="${volumes_data[1].readwrite}" selected hidden>${volumes_data[1].readwrite}</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume2" type="checkbox" ${volumes_data[2].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_2_bind" value="${volumes_data[2].bind}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_2_container" value="${volumes_data[2].container}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_2_readwrite">
|
||||
<option value="${volumes_data[2].readwrite}" selected hidden>${volumes_data[2].readwrite}</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume3" type="checkbox" ${volumes_data[3].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_3_bind" value="${volumes_data[3].bind}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_3_container" value="${volumes_data[3].container}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_3_readwrite">
|
||||
<option value="${volumes_data[3].readwrite}" selected hidden>${volumes_data[3].readwrite}</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume4" type="checkbox" ${volumes_data[4].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_4_bind" value="${volumes_data[4].bind}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_4_container" value="${volumes_data[4].container}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_4_readwrite">
|
||||
<option value="${volumes_data[4].readwrite}" selected hidden>${volumes_data[4].readwrite}</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume5" type="checkbox" ${volumes_data[5].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_5_bind" value="${volumes_data[5].bind}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_5_container" value="${volumes_data[5].container}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_5_readwrite">
|
||||
<option value="${volumes_data[5].readwrite}" selected hidden>${volumes_data[5].readwrite}</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-3">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-3" aria-expanded="false">
|
||||
Environment Variables
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-3" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env0" ${env_data[0].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Variable</label>
|
||||
<input type="text" class="form-control" name="env_0_name" value="${env_data[0].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" name="env_0_default" value="${env_data[0].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env1" ${env_data[1].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_1_name" value="${env_data[1].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_1_default" value="${env_data[1].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env2" ${env_data[2].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_2_name" value="${env_data[2].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_2_default" value="${env_data[2].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env3" ${env_data[3].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_3_name" value="${env_data[3].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_3_default" value="${env_data[3].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env4" ${env_data[4].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_4_name" value="${env_data[4].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_4_default" value="${env_data[4].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env5" ${env_data[5].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_5_name" value="${env_data[5].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_5_default" value="${env_data[5].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env6" ${env_data[6].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_6_name" value="${env_data[6].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_6_default" value="${env_data[6].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env7" ${env_data[7].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_7_name" value="${env_data[7].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_7_default" value="${env_data[7].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env8" ${env_data[8].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_8_name" value="${env_data[8].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_8_default" value="${env_data[8].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env9" ${env_data[9].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_9_name" value="${env_data[9].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_9_default" value="${env_data[9].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env10" ${env_data[10].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_10_name" value="${env_data[10].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_10_default" value="${env_data[10].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env11" ${env_data[11].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_11_name" value="${env_data[11].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_11_default" value="${env_data[11].default}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-4">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-4" aria-expanded="false">
|
||||
Labels
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-4" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label0" ${label_data[0].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Variable</label>
|
||||
<input type="text" class="form-control" name="label_0_name" value="${label_data[0].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" name="label_0_value" value="${label_data[0].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label1" ${label_data[1].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_1_name" value="${label_data[1].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_1_value" value="${label_data[1].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label2" ${label_data[2].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_2_name" value="${label_data[2].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_2_value" value="${label_data[2].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label3" ${label_data[3].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_3_name" value="${label_data[3].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_3_value" value="${label_data[3].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label4" ${label_data[4].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_4_name" value="${label_data[4].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_4_value" value="${label_data[4].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label5" ${label_data[5].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_5_name" value="${label_data[5].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_5_value" value="${label_data[5].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label6" ${label_data[6].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_6_name" value="${label_data[6].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_6_value" value="${label_data[6].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label7" ${label_data[7].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_7_name" value="${label_data[7].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_7_value" value="${label_data[7].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label8" ${label_data[8].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_8_name" value="${label_data[8].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_8_value" value="${label_data[8].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label9" ${label_data[9].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_9_name" value="${label_data[9].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_9_value" value="${label_data[9].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label10" ${label_data[10].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_10_name" value="${label_data[10].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_10_value" value="${label_data[10].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label11" ${label_data[11].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="${label_data[11].name}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="${label_data[11].value}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-5">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
|
||||
Extras
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#${modal}-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="command_check" type="checkbox" ${command_check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Command</label>
|
||||
<input type="text" class="form-control" name="command" value="${command}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="privileged" type="checkbox" ${privileged_check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Privileged Mode</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
|
||||
<input type="submit" form="${form_id}_install" class="btn btn-success" value="Install"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
}
|
|
@ -1,285 +0,0 @@
|
|||
// export for app.js
|
||||
export const containerCard = (data) => {
|
||||
|
||||
let { name, service, state, external_port, internal_port, ports, link } = data;
|
||||
let wrapped = name;
|
||||
let chart = name;
|
||||
|
||||
if (name.length > 13) {
|
||||
wrapped = name.slice(0, 10) + '...';
|
||||
}
|
||||
|
||||
//disable controls for a docker container depending on its name
|
||||
let actions = "";
|
||||
if (name.startsWith('dweebui')) {
|
||||
actions = 'disabled=""';
|
||||
}
|
||||
|
||||
if ( external_port == undefined ) { external_port = 0; }
|
||||
if ( internal_port == undefined ) { internal_port = 0; }
|
||||
|
||||
|
||||
let state_indicator = 'green';
|
||||
if (state == 'exited') {
|
||||
state = 'stopped';
|
||||
state_indicator = 'red';
|
||||
} else if (state == 'paused') {
|
||||
state_indicator = 'orange';
|
||||
}
|
||||
|
||||
let ports_data = [];
|
||||
if (ports) {
|
||||
ports_data = ports;
|
||||
} else {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
|
||||
let port_check = "checked";
|
||||
let external = i;
|
||||
let internal = i;
|
||||
let protocol = "tcp";
|
||||
|
||||
ports_data.push({
|
||||
check: port_check,
|
||||
external: external,
|
||||
internal: internal,
|
||||
protocol: protocol
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return `
|
||||
<div class="col-sm-6 col-lg-3 deleteme">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-stamp card-stamp-sm">
|
||||
<img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service}.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader text-yellow">${external_port}:${internal_port}</div>
|
||||
<div class="ms-auto lh-1">
|
||||
<div class="card-actions btn-actions">
|
||||
<div class="card-actions btn-actions">
|
||||
<button onclick="clicked(this)" name="${name}" value="${state}" id="start" class="btn-action" title="Start" ${actions}><!-- player-play -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
|
||||
</button>
|
||||
<button onclick="clicked(this)" name="${name}" value="${state}" id="stop" class="btn-action" title="Stop" ${actions}><!-- player-stop -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
|
||||
</button>
|
||||
<button onclick="clicked(this)" name="${name}" value="${state}" id="pause" class="btn-action" title="Pause" ${actions}><!-- player-pause -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
|
||||
</button>
|
||||
<button onclick="clicked(this)" name="${name}" value="${state}" id="restart" class="btn-action" title="Restart" ${actions}><!-- reload -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item text-secondary" onclick="clicked(this)" id="details" data-bs-toggle="modal" data-bs-target="#details_modal" href="#">Details</button>
|
||||
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="logs" data-bs-toggle="modal" data-bs-target="#log_view" href="#">Logs</button>
|
||||
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="edit" href="#">Edit</button>
|
||||
<button class="dropdown-item text-primary" onclick="clicked(this)" name="${name}" id="update" href="#">Update</button>
|
||||
<button class="dropdown-item text-danger" onclick="clicked(this)" name="${name}" id="remove" data-bs-toggle="modal" data-bs-target="#${name}_uninstall_modal" href="#">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="hide" value="hide">Hide</button>
|
||||
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="resetView" value="resetView">Reset View</button>
|
||||
<button class="dropdown-item text-secondary" onclick="clicked(this)" name="${name}" id="permissions" value="permissions" data-bs-toggle="modal" data-bs-target="#${name}_permissions">Permissions</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline">
|
||||
<div class="h1 me-2" title="${name}" style="margin-bottom: 0;">
|
||||
<a href="http://${link}:${external_port}" target="_blank">
|
||||
${wrapped}
|
||||
</a>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<span class="text-${state_indicator} align-items-center lh-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
|
||||
${state}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="${chart}_chart" class="chart-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal modal-blur fade" id="${name}_uninstall_modal" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<div class="modal-status bg-danger"></div>
|
||||
<div class="modal-body text-center py-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 9v2m0 4v.01"></path><path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path></svg>
|
||||
<h3>Remove ${name}?</h3>
|
||||
<form action="/uninstall" id="${name}_uninstall" method="POST">
|
||||
<input type="text" class="form-control" name="service_name" value="${name}" hidden/>
|
||||
<div class="mb-3"> </div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="divide-y">
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">Remove Volumes</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_volumes" disabled="">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Remove Image
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_image" disabled="">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Remove Backups
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_backups" disabled="">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1"> </div>
|
||||
<div class="text-muted">Enter "Yes" below to remove the container.</div>
|
||||
<input type="text" class="form-control mb-2" name="confirm" autocomplete="off">
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="w-100">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="#" class="btn w-100" data-bs-dismiss="modal">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="submit" form="${name}_uninstall" class="btn btn-danger w-100" value="Uninstall"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal modal-blur fade" id="${name}_permissions" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<div class="modal-status bg-cyan"></div>
|
||||
<div class="modal-body text-center py-3">
|
||||
<h3>${name} permissions</h3>
|
||||
<form action="#" id="${name}_permissions" method="POST">
|
||||
<input type="text" class="form-control" name="service_name" value="${name}" hidden/>
|
||||
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="divide-y">
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">Install</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_volumes">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Uninstall
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_image">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Edit
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_backups">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Upgrade
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_backups">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1"> </div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="w-100">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="#" class="btn w-100" data-bs-dismiss="modal">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="submit" form="${name}_permissions" class="btn btn-primary w-100" value="Update"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
|
@ -1,24 +1,27 @@
|
|||
version: "3.9"
|
||||
services:
|
||||
dweebui:
|
||||
container_name: dweebui
|
||||
image: lllllllillllllillll/dweebui:v0.20
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 8000
|
||||
SECRET: MrWiskers
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- dweebui:/app
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
networks:
|
||||
- dweebui_net
|
||||
|
||||
volumes:
|
||||
dweebui:
|
||||
|
||||
networks:
|
||||
dweebui_net:
|
||||
driver: bridge
|
||||
version: "3.9"
|
||||
services:
|
||||
dweebui:
|
||||
container_name: dweebui
|
||||
image: lllllllillllllillll/dweebui:v0.60
|
||||
environment:
|
||||
PORT: 8000
|
||||
SECRET: MrWiskers
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8000:8000
|
||||
volumes:
|
||||
- dweebui:/app/config
|
||||
# Docker socket
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# Podman socket
|
||||
#- /run/podman/podman.sock:/var/run/docker.sock
|
||||
|
||||
networks:
|
||||
- dweebui_net
|
||||
|
||||
volumes:
|
||||
dweebui:
|
||||
|
||||
networks:
|
||||
dweebui_net:
|
||||
driver: bridge
|
|
@ -12,7 +12,8 @@ export const Account = async (req, res) => {
|
|||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
alert: '',
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -1,98 +1,587 @@
|
|||
import { readFileSync } from 'fs';
|
||||
import { appCard } from '../components/appCard.js';
|
||||
import { readFileSync, readdirSync, renameSync, mkdirSync, unlinkSync, existsSync } from 'fs';
|
||||
import { parse } from 'yaml';
|
||||
import multer from 'multer';
|
||||
import AdmZip from 'adm-zip';
|
||||
|
||||
let templatesJSON = readFileSync('./templates.json');
|
||||
let templates = JSON.parse(templatesJSON).templates;
|
||||
const upload = multer({storage: multer.diskStorage({
|
||||
destination: function (req, file, cb) { cb(null, 'templates/tmp/') },
|
||||
filename: function (req, file, cb) { cb(null, file.originalname) },
|
||||
})});
|
||||
|
||||
templates = templates.sort((a, b) => {
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
let alert = '';
|
||||
let templates_global = '';
|
||||
let json_templates = '';
|
||||
let remove_button = '';
|
||||
|
||||
export const Apps = async (req, res) => {
|
||||
|
||||
let page = Number(req.params.page) || 1;
|
||||
let template_param = req.params.template || 'default';
|
||||
|
||||
if ((template_param != 'default') && (template_param != 'compose')) {
|
||||
remove_button = `<a href="/remove_template/${template_param}" class="btn" hx-confirm="Are you sure you want to remove this template?">Remove</a>`;
|
||||
} else {
|
||||
remove_button = '';
|
||||
}
|
||||
|
||||
json_templates = '';
|
||||
let json_files = readdirSync('templates/json/');
|
||||
for (let i = 0; i < json_files.length; i++) {
|
||||
if (json_files[i] != 'default.json') {
|
||||
let filename = json_files[i].split('.')[0];
|
||||
let link = `<li><a class="dropdown-item" href="/apps/1/${filename}">${filename}</a></li>`
|
||||
json_templates += link;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const Apps = (req, res) => {
|
||||
let apps_list = '';
|
||||
let app_count = '';
|
||||
|
||||
let list_start = (page - 1) * 28;
|
||||
let list_end = (page * 28);
|
||||
let last_page = '';
|
||||
|
||||
let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
|
||||
|
||||
|
||||
let prev = '/apps/' + (page - 1) + '/' + template_param;
|
||||
let next = '/apps/' + (page + 1) + '/' + template_param;
|
||||
if (page == 1) { prev = '/apps/' + (page) + '/' + template_param; }
|
||||
if (page == last_page) { next = '/apps/' + (page) + '/' + template_param;}
|
||||
|
||||
|
||||
if (template_param == 'compose') {
|
||||
let compose_files = readdirSync('templates/compose/');
|
||||
|
||||
let page = Number(req.params.page) || 1;
|
||||
let list_start = (page-1)*28;
|
||||
let list_end = (page*28);
|
||||
let last_page = Math.ceil(templates.length/28);
|
||||
app_count = compose_files.length;
|
||||
last_page = Math.ceil(compose_files.length/28);
|
||||
|
||||
let prev = '/apps/' + (page-1);
|
||||
let next = '/apps/' + (page+1);
|
||||
if (page == 1) {
|
||||
prev = '/apps/' + (page);
|
||||
}
|
||||
if (page == last_page) {
|
||||
next = '/apps/' + (page);
|
||||
}
|
||||
compose_files.forEach(file => {
|
||||
if (file == '.gitignore') { return; }
|
||||
|
||||
let apps_list = '';
|
||||
for (let i = list_start; i < list_end && i < templates.length; i++) {
|
||||
let app_card = appCard(templates[i]);
|
||||
let compose = readFileSync(`templates/compose/${file}/compose.yaml`, 'utf8');
|
||||
let compose_data = parse(compose);
|
||||
let service_name = Object.keys(compose_data.services)
|
||||
let container = compose_data.services[service_name].container_name;
|
||||
let image = compose_data.services[service_name].image;
|
||||
|
||||
apps_list += app_card;
|
||||
}
|
||||
|
||||
res.render("apps", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
list_start: list_start + 1,
|
||||
list_end: list_end,
|
||||
app_count: templates.length,
|
||||
prev: prev,
|
||||
next: next,
|
||||
apps_list: apps_list
|
||||
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
|
||||
appCard = appCard.replace(/AppName/g, service_name);
|
||||
appCard = appCard.replace(/AppShortName/g, service_name);
|
||||
appCard = appCard.replace(/AppDesc/g, 'Compose File');
|
||||
appCard = appCard.replace(/AppLogo/g, `https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/${service_name}.png`);
|
||||
appCard = appCard.replace(/AppCategories/g, '<span class="badge bg-orange-lt">Compose</span> ');
|
||||
appCard = appCard.replace(/AppType/g, 'compose');
|
||||
apps_list += appCard;
|
||||
});
|
||||
} else {
|
||||
|
||||
let template_file = readFileSync(`./templates/json/${template_param}.json`);
|
||||
let templates = JSON.parse(template_file).templates;
|
||||
templates = templates.sort((a, b) => { if (a.name < b.name) { return -1; } });
|
||||
app_count = templates.length;
|
||||
|
||||
templates_global = templates;
|
||||
|
||||
apps_list = '';
|
||||
for (let i = list_start; i < list_end && i < templates.length; i++) {
|
||||
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
|
||||
let name = templates[i].name || templates[i].title.toLowerCase();
|
||||
let title = templates[i].title || templates[i].name;
|
||||
let desc = templates[i].description.slice(0, 60) + "...";
|
||||
let description = templates[i].description.replaceAll(". ", ".\n") || "no description available";
|
||||
let note = templates[i].note ? templates[i].note.replaceAll(". ", ".\n") : "no notes available";
|
||||
let image = templates[i].image;
|
||||
let logo = templates[i].logo;
|
||||
let categories = '';
|
||||
// set data.catagories to 'other' if data.catagories is empty or undefined
|
||||
if (templates[i].categories == null || templates[i].categories == undefined || templates[i].categories == '') {
|
||||
templates[i].categories = ['Other'];
|
||||
}
|
||||
// loop through the categories and add the badge to the card
|
||||
for (let j = 0; j < templates[i].categories.length; j++) {
|
||||
categories += CatagoryColor(templates[i].categories[j]);
|
||||
}
|
||||
appCard = appCard.replace(/AppName/g, name);
|
||||
appCard = appCard.replace(/AppTitle/g, title);
|
||||
appCard = appCard.replace(/AppShortName/g, name);
|
||||
appCard = appCard.replace(/AppDesc/g, desc);
|
||||
appCard = appCard.replace(/AppLogo/g, logo);
|
||||
appCard = appCard.replace(/AppCategories/g, categories);
|
||||
appCard = appCard.replace(/AppType/g, 'json');
|
||||
apps_list += appCard;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
res.render("apps", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
list_start: list_start + 1,
|
||||
list_end: list_end,
|
||||
app_count: app_count,
|
||||
prev: prev,
|
||||
next: next,
|
||||
apps_list: apps_list,
|
||||
alert: alert,
|
||||
template_list: '',
|
||||
json_templates: json_templates,
|
||||
pages: pages,
|
||||
remove_button: remove_button
|
||||
});
|
||||
alert = '';
|
||||
}
|
||||
|
||||
export const removeTemplate = async (req, res) => {
|
||||
let template = req.params.template;
|
||||
unlinkSync(`templates/json/${template}.json`);
|
||||
res.redirect('/apps');
|
||||
}
|
||||
|
||||
|
||||
export const appSearch = async (req, res) => {
|
||||
|
||||
let search = req.body.search.split(' ');
|
||||
let apps_list = '';
|
||||
let results = [];
|
||||
let search = req.body.search;
|
||||
|
||||
let page = Number(req.query.page) || 1;
|
||||
let list_start = (page - 1) * 28;
|
||||
let list_end = (page * 28);
|
||||
let last_page = Math.ceil(templates.length / 28);
|
||||
let page = Number(req.params.page) || 1;
|
||||
|
||||
let prev = '/apps?page=' + (page - 1);
|
||||
let next = '/apps?page=' + (page + 1);
|
||||
if (page == 1) {
|
||||
prev = '/apps?page=' + (page);
|
||||
}
|
||||
if (page == last_page) {
|
||||
next = '/apps?page=' + (page);
|
||||
}
|
||||
let template_param = req.params.template || 'default';
|
||||
|
||||
function searchTemplates(word) {
|
||||
for (let i = 0; i < templates.length; i++) {
|
||||
if ((templates[i].description.includes(word)) || (templates[i].name.includes(word)) || (templates[i].title.includes(word))) {
|
||||
results.push(templates[i]);
|
||||
}
|
||||
let template_file = readFileSync(`./templates/json/${template_param}.json`);
|
||||
|
||||
let templates = JSON.parse(template_file).templates;
|
||||
|
||||
templates = templates.sort((a, b) => {
|
||||
if (a.name < b.name) { return -1; }
|
||||
});
|
||||
|
||||
let pages = `<li class="page-item"><a class="page-link" href="/apps/1/${template_param}">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/2/${template_param}">2</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/3/${template_param}">3</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/4/${template_param}">4</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps/5/${template_param}">5</a></li>`
|
||||
|
||||
|
||||
let list_start = (page-1)*28;
|
||||
let list_end = (page*28);
|
||||
let last_page = Math.ceil(templates.length/28);
|
||||
let prev = '/apps/' + (page-1);
|
||||
let next = '/apps/' + (page+1);
|
||||
if (page == 1) { prev = '/apps/' + (page); }
|
||||
if (page == last_page) { next = '/apps/' + (page); }
|
||||
|
||||
|
||||
let apps_list = '';
|
||||
let results = [];
|
||||
let [cat_1, cat_2, cat_3] = ['','',''];
|
||||
|
||||
function searchTemplates(terms) {
|
||||
terms = terms.toLowerCase();
|
||||
for (let i = 0; i < templates.length; i++) {
|
||||
if (templates[i].categories) {
|
||||
if (templates[i].categories[0]) {
|
||||
cat_1 = (templates[i].categories[0]).toLowerCase();
|
||||
}
|
||||
if (templates[i].categories[1]) {
|
||||
cat_2 = (templates[i].categories[1]).toLowerCase();
|
||||
}
|
||||
if (templates[i].categories[2]) {
|
||||
cat_3 = (templates[i].categories[2]).toLowerCase();
|
||||
}
|
||||
}
|
||||
if ((templates[i].description.includes(terms)) || (templates[i].name.includes(terms)) || (templates[i].title.includes(terms)) || (cat_1.includes(terms)) || (cat_2.includes(terms)) || (cat_3.includes(terms))){
|
||||
results.push(templates[i]);
|
||||
}
|
||||
}
|
||||
searchTemplates(search);
|
||||
}
|
||||
searchTemplates(search);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
let appCard = readFileSync('./views/partials/appCard.html', 'utf8');
|
||||
let name = results[i].name || results[i].title.toLowerCase();
|
||||
let desc = results[i].description.slice(0, 60) + "...";
|
||||
let description = results[i].description.replaceAll(". ", ".\n") || "no description available";
|
||||
let note = results[i].note ? results[i].note.replaceAll(". ", ".\n") : "no notes available";
|
||||
let image = results[i].image;
|
||||
let logo = results[i].logo;let categories = '';
|
||||
// set data.catagories to 'other' if data.catagories is empty or undefined
|
||||
if (results[i].categories == null || results[i].categories == undefined || results[i].categories == '') {
|
||||
results[i].categories = ['Other'];
|
||||
}
|
||||
// loop through the categories and add the badge to the card
|
||||
for (let j = 0; j < results[i].categories.length; j++) {
|
||||
categories += CatagoryColor(results[i].categories[j]);
|
||||
}
|
||||
appCard = appCard.replace(/AppName/g, name);
|
||||
appCard = appCard.replace(/AppShortName/g, name);
|
||||
appCard = appCard.replace(/AppDesc/g, desc);
|
||||
appCard = appCard.replace(/AppLogo/g, logo);
|
||||
appCard = appCard.replace(/AppCategories/g, categories);
|
||||
appCard = appCard.replace(/AppType/g, 'json');
|
||||
|
||||
apps_list += appCard;
|
||||
}
|
||||
res.render("apps", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
list_start: list_start + 1,
|
||||
list_end: list_end,
|
||||
app_count: results.length,
|
||||
prev: prev,
|
||||
next: next,
|
||||
apps_list: apps_list,
|
||||
alert: alert,
|
||||
template_list: '',
|
||||
json_templates: json_templates,
|
||||
pages: pages,
|
||||
remove_button: remove_button
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function CatagoryColor(category) {
|
||||
switch (category) {
|
||||
case 'Other':
|
||||
return '<span class="badge bg-blue-lt">Other</span> ';
|
||||
case 'Productivity':
|
||||
return '<span class="badge bg-blue-lt">Productivity</span> ';
|
||||
case 'Tools':
|
||||
return '<span class="badge bg-blue-lt">Tools</span> ';
|
||||
case 'Dashboard':
|
||||
return '<span class="badge bg-blue-lt">Dashboard</span> ';
|
||||
case 'Communication':
|
||||
return '<span class="badge bg-azure-lt">Communication</span> ';
|
||||
case 'Media':
|
||||
return '<span class="badge bg-azure-lt">Media</span> ';
|
||||
case 'CMS':
|
||||
return '<span class="badge bg-azure-lt">CMS</span> ';
|
||||
case 'Monitoring':
|
||||
return '<span class="badge bg-indigo-lt">Monitoring</span> ';
|
||||
case 'LDAP':
|
||||
return '<span class="badge bg-purple-lt">LDAP</span> ';
|
||||
case 'Arr':
|
||||
return '<span class="badge bg-purple-lt">Arr</span> ';
|
||||
case 'Database':
|
||||
return '<span class="badge bg-red-lt">Database</span> ';
|
||||
case 'Paid':
|
||||
return '<span class="badge bg-red-lt" title="This is a paid product or contains paid features.">Paid</span> ';
|
||||
case 'Gaming':
|
||||
return '<span class="badge bg-pink-lt">Gaming</span> ';
|
||||
case 'Finance':
|
||||
return '<span class="badge bg-orange-lt">Finance</span> ';
|
||||
case 'Networking':
|
||||
return '<span class="badge bg-yellow-lt">Networking</span> ';
|
||||
case 'Authentication':
|
||||
return '<span class="badge bg-lime-lt">Authentication</span> ';
|
||||
case 'Development':
|
||||
return '<span class="badge bg-green-lt">Development</span> ';
|
||||
case 'Media Server':
|
||||
return '<span class="badge bg-teal-lt">Media Server</span> ';
|
||||
case 'Downloaders':
|
||||
return '<span class="badge bg-cyan-lt">Downloaders</span> ';
|
||||
default:
|
||||
return ''; // default to other if the category is not recognized
|
||||
}
|
||||
}
|
||||
|
||||
export const InstallModal = async (req, res) => {
|
||||
let input = req.header('hx-trigger-name');
|
||||
let type = req.header('hx-trigger');
|
||||
|
||||
if (type == 'compose') {
|
||||
let compose = readFileSync(`templates/compose/${input}/compose.yaml`, 'utf8');
|
||||
let modal = readFileSync('./views/modals/compose.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, input);
|
||||
modal = modal.replace(/COMPOSE_CONTENT/g, compose);
|
||||
res.send(modal);
|
||||
return;
|
||||
} else {
|
||||
let result = templates_global.find(t => t.name == input);
|
||||
let name = result.name || result.title.toLowerCase();
|
||||
let short_name = name.slice(0, 25) + "...";
|
||||
let desc = result.description.replaceAll(". ", ".\n") || "no description available";
|
||||
let short_desc = desc.slice(0, 60) + "...";
|
||||
let modal_name = name.replaceAll(" ", "-");
|
||||
let form_id = name.replaceAll("-", "_");
|
||||
let note = result.note ? result.note.replaceAll(". ", ".\n") : "no notes available";
|
||||
let command = result.command ? result.command : "";
|
||||
let command_check = command ? "checked" : "";
|
||||
let privileged = result.privileged || "";
|
||||
let privileged_check = privileged ? "checked" : "";
|
||||
let repository = result.repository || "";
|
||||
let image = result.image || "";
|
||||
let net_host, net_bridge, net_docker = '';
|
||||
let net_name = 'AppBridge';
|
||||
let restart_policy = result.restart_policy || 'unless-stopped';
|
||||
|
||||
switch (result.network) {
|
||||
case 'host':
|
||||
net_host = 'checked';
|
||||
break;
|
||||
case 'bridge':
|
||||
net_bridge = 'checked';
|
||||
net_name = result.network;
|
||||
break;
|
||||
default:
|
||||
net_docker = 'checked';
|
||||
}
|
||||
|
||||
if (repository != "") {
|
||||
image = (`${repository.url}/raw/master/${repository.stackfile}`);
|
||||
}
|
||||
|
||||
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
|
||||
// Get port details
|
||||
try {
|
||||
let ports = result.ports[i];
|
||||
let port_check = ports ? "checked" : "";
|
||||
let port_external = ports.split(":")[0] ? ports.split(":")[0] : ports.split("/")[0];
|
||||
let port_internal = ports.split(":")[1] ? ports.split(":")[1].split("/")[0] : ports.split("/")[0];
|
||||
let port_protocol = ports.split("/")[1] ? ports.split("/")[1] : "";
|
||||
|
||||
// remove /tcp or /udp from port_external if it exists
|
||||
if (port_external.includes("/")) {
|
||||
port_external = port_external.split("/")[0];
|
||||
}
|
||||
|
||||
ports_data.push({
|
||||
check: port_check,
|
||||
external: port_external,
|
||||
internal: port_internal,
|
||||
protocol: port_protocol
|
||||
});
|
||||
} catch {
|
||||
ports_data.push({
|
||||
check: "",
|
||||
external: "",
|
||||
internal: "",
|
||||
protocol: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get volume details
|
||||
try {
|
||||
let volumes = result.volumes[i];
|
||||
let volume_check = volumes ? "checked" : "";
|
||||
let volume_bind = volumes.bind ? volumes.bind : "";
|
||||
let volume_container = volumes.container ? volumes.container.split(":")[0] : "";
|
||||
let volume_readwrite = "rw";
|
||||
|
||||
if (volumes.readonly == true) {
|
||||
volume_readwrite = "ro";
|
||||
}
|
||||
|
||||
volumes_data.push({
|
||||
check: volume_check,
|
||||
bind: volume_bind,
|
||||
container: volume_container,
|
||||
readwrite: volume_readwrite
|
||||
});
|
||||
} catch {
|
||||
volumes_data.push({
|
||||
check: "",
|
||||
bind: "",
|
||||
container: "",
|
||||
readwrite: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get environment details
|
||||
try {
|
||||
let env = result.env[i];
|
||||
let env_check = "";
|
||||
let env_default = env.default ? env.default : "";
|
||||
if (env.set) { env_default = env.set;}
|
||||
let env_description = env.description ? env.description : "";
|
||||
let env_label = env.label ? env.label : "";
|
||||
let env_name = env.name ? env.name : "";
|
||||
|
||||
env_data.push({
|
||||
check: env_check,
|
||||
default: env_default,
|
||||
description: env_description,
|
||||
label: env_label,
|
||||
name: env_name
|
||||
});
|
||||
} catch {
|
||||
env_data.push({
|
||||
check: "",
|
||||
default: "",
|
||||
description: "",
|
||||
label: "",
|
||||
name: ""
|
||||
});
|
||||
}
|
||||
|
||||
// Get label details
|
||||
try {
|
||||
let label = result.labels[i];
|
||||
let label_check = "";
|
||||
let label_name = label.name ? label.name : "";
|
||||
let label_value = label.value ? label.value : "";
|
||||
|
||||
label_data.push({
|
||||
check: label_check,
|
||||
name: label_name,
|
||||
value: label_value
|
||||
});
|
||||
} catch {
|
||||
label_data.push({
|
||||
check: "",
|
||||
name: "",
|
||||
value: ""
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
let app_card = appCard(results[i]);
|
||||
apps_list += app_card;
|
||||
}
|
||||
|
||||
res.render("apps", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
list_start: list_start + 1,
|
||||
list_end: list_end,
|
||||
app_count: templates.length,
|
||||
prev: prev,
|
||||
next: next,
|
||||
apps_list: apps_list
|
||||
});
|
||||
let modal = readFileSync('./views/modals/json.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, name);
|
||||
modal = modal.replace(/AppNote/g, note);
|
||||
modal = modal.replace(/AppImage/g, image);
|
||||
modal = modal.replace(/RestartPolicy/g, restart_policy);
|
||||
modal = modal.replace(/NetHost/g, net_host);
|
||||
modal = modal.replace(/NetBridge/g, net_bridge);
|
||||
modal = modal.replace(/NetDocker/g, net_docker);
|
||||
modal = modal.replace(/NetName/g, net_name);
|
||||
modal = modal.replace(/ModalName/g, modal_name);
|
||||
modal = modal.replace(/FormId/g, form_id);
|
||||
modal = modal.replace(/CommandCheck/g, command_check);
|
||||
modal = modal.replace(/CommandValue/g, command);
|
||||
modal = modal.replace(/PrivilegedCheck/g, privileged_check);
|
||||
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
modal = modal.replaceAll(`Port${i}Check`, ports_data[i].check);
|
||||
modal = modal.replaceAll(`Port${i}External`, ports_data[i].external);
|
||||
modal = modal.replaceAll(`Port${i}Internal`, ports_data[i].internal);
|
||||
modal = modal.replaceAll(`Port${i}Protocol`, ports_data[i].protocol);
|
||||
|
||||
modal = modal.replaceAll(`Volume${i}Check`, volumes_data[i].check);
|
||||
modal = modal.replaceAll(`Volume${i}Bind`, volumes_data[i].bind);
|
||||
modal = modal.replaceAll(`Volume${i}Container`, volumes_data[i].container);
|
||||
modal = modal.replaceAll(`Volume${i}RW`, volumes_data[i].readwrite);
|
||||
|
||||
modal = modal.replaceAll(`Env${i}Check`, env_data[i].check);
|
||||
modal = modal.replaceAll(`Env${i}Default`, env_data[i].default);
|
||||
modal = modal.replaceAll(`Env${i}Description`, env_data[i].description);
|
||||
modal = modal.replaceAll(`Env${i}Label`, env_data[i].label);
|
||||
modal = modal.replaceAll(`Env${i}Name`, env_data[i].name);
|
||||
|
||||
modal = modal.replaceAll(`Label${i}Check`, label_data[i].check);
|
||||
modal = modal.replaceAll(`Label${i}Name`, label_data[i].name);
|
||||
modal = modal.replaceAll(`Label${i}Value`, label_data[i].value);
|
||||
}
|
||||
res.send(modal);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const LearnMore = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let id = req.header('hx-trigger');
|
||||
|
||||
if (id == 'compose') {
|
||||
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, name);
|
||||
modal = modal.replace(/AppDesc/g, 'Compose File');
|
||||
res.send(modal);
|
||||
return;
|
||||
}
|
||||
|
||||
let result = templates_global.find(t => t.name == name);
|
||||
|
||||
let modal = readFileSync('./views/modals/learnmore.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, result.title);
|
||||
modal = modal.replace(/AppDesc/g, result.description);
|
||||
|
||||
res.send(modal);
|
||||
}
|
||||
|
||||
|
||||
export const ImportModal = async (req, res) => {
|
||||
let modal = readFileSync('./views/modals/import.html', 'utf8');
|
||||
res.send(modal);
|
||||
}
|
||||
|
||||
|
||||
export const Upload = (req, res) => {
|
||||
upload.array('files', 10)(req, res, () => {
|
||||
|
||||
alert = `<div class="alert alert-success alert-dismissible mb-0 py-2" role="alert">
|
||||
<div class="d-flex">
|
||||
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg> </div>
|
||||
<div>Template(s) Uploaded!</div>
|
||||
</div>
|
||||
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
|
||||
</div>`;
|
||||
|
||||
|
||||
let exists_alert = `<div class="alert alert-danger alert-dismissible mb-0 py-2" role="alert">
|
||||
<div class="d-flex">
|
||||
<div><svg xmlns="http://www.w3.org/2000/svg" class="icon alert-icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l5 5l10 -10"></path></svg> </div>
|
||||
<div>Template already exists</div>
|
||||
</div>
|
||||
<a class="btn-close" data-bs-dismiss="alert" aria-label="close" style="padding-top: 0.5rem;"></a>
|
||||
</div>`;
|
||||
|
||||
let files = readdirSync('templates/tmp/');
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
|
||||
if (files[i].endsWith('.zip')) {
|
||||
let zip = new AdmZip(`templates/tmp/${files[i]}`);
|
||||
zip.extractAllTo('templates/compose', true);
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
} else if (files[i].endsWith('.json')) {
|
||||
if (existsSync(`templates/json/${files[i]}`)) {
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
alert = exists_alert;
|
||||
res.redirect('/apps');
|
||||
return;
|
||||
}
|
||||
renameSync(`templates/tmp/${files[i]}`, `templates/json/${files[i]}`);
|
||||
} else if (files[i].endsWith('.yml')) {
|
||||
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
|
||||
let compose_data = parse(compose);
|
||||
let service_name = Object.keys(compose_data.services);
|
||||
if (existsSync(`templates/compose/${service_name}`)) {
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
alert = exists_alert;
|
||||
res.redirect('/apps');
|
||||
return;
|
||||
}
|
||||
mkdirSync(`templates/compose/${service_name}`);
|
||||
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
|
||||
} else if (files[i].endsWith('.yaml')) {
|
||||
let compose = readFileSync(`templates/tmp/${files[i]}`, 'utf8');
|
||||
let compose_data = parse(compose);
|
||||
let service_name = Object.keys(compose_data.services);
|
||||
if (existsSync(`templates/compose/${service_name}`)) {
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
alert = exists_alert;
|
||||
res.redirect('/apps');
|
||||
return;
|
||||
}
|
||||
mkdirSync(`templates/compose/${service_name}`);
|
||||
renameSync(`templates/tmp/${files[i]}`, `templates/compose/${service_name}/compose.yaml`);
|
||||
} else {
|
||||
// unsupported file type
|
||||
unlinkSync(`templates/tmp/${files[i]}`);
|
||||
}
|
||||
}
|
||||
res.redirect('/apps');
|
||||
});
|
||||
};
|
|
@ -1,23 +1,448 @@
|
|||
import { Readable } from 'stream';
|
||||
import { Permission, User } from '../database/models.js';
|
||||
import { docker } from '../server.js';
|
||||
import { dockerContainerStats } from 'systeminformation';
|
||||
import { readFileSync } from 'fs';
|
||||
import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
let hidden = '';
|
||||
let alert = '';
|
||||
let [ cardList, newCards, stats ] = [ '', '', {}];
|
||||
let [ports_data, volumes_data, env_data, label_data] = [[], [], [], []];
|
||||
|
||||
// The page
|
||||
export const Dashboard = (req, res) => {
|
||||
|
||||
|
||||
let name = req.session.user;
|
||||
let role = req.session.role;
|
||||
alert = req.session.alert;
|
||||
|
||||
res.render("dashboard", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
name: name,
|
||||
avatar: name.charAt(0).toUpperCase(),
|
||||
role: role,
|
||||
alert: alert,
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export const searchDashboard = (req, res) => {
|
||||
// The page actions
|
||||
export const DashboardAction = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let value = req.header('hx-trigger');
|
||||
let action = req.params.action;
|
||||
let modal = '';
|
||||
|
||||
console.log(req.params);
|
||||
switch (action) {
|
||||
case 'permissions':
|
||||
let title = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
let permissions_list = '';
|
||||
let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
|
||||
permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
|
||||
permissions_modal = permissions_modal.replace(/PermissionsContainer/g, name);
|
||||
let users = await User.findAll({ attributes: ['username', 'UUID']});
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
|
||||
let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
|
||||
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID}); }
|
||||
let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
|
||||
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
|
||||
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
|
||||
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
|
||||
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
|
||||
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
|
||||
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
|
||||
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
|
||||
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
|
||||
if (permissions.view == true) { user_permissions = user_permissions.replace(/data-ViewCheck/g, 'checked'); }
|
||||
user_permissions = user_permissions.replace(/EntryNumber/g, i);
|
||||
user_permissions = user_permissions.replace(/EntryNumber/g, i);
|
||||
user_permissions = user_permissions.replace(/EntryNumber/g, i);
|
||||
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
|
||||
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
|
||||
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
|
||||
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
|
||||
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
|
||||
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
|
||||
permissions_list += user_permissions;
|
||||
}
|
||||
permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
|
||||
res.send(permissions_modal);
|
||||
return;
|
||||
case 'uninstall':
|
||||
modal = readFileSync('./views/modals/uninstall.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, name);
|
||||
res.send(modal);
|
||||
return;
|
||||
case 'details':
|
||||
modal = readFileSync('./views/modals/details.html', 'utf8');
|
||||
let details = await containerInfo(name);
|
||||
|
||||
res.render("dashboard", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
modal = modal.replace(/AppName/g, details.name);
|
||||
modal = modal.replace(/AppImage/g, details.image);
|
||||
|
||||
for (let i = 0; i <= 6; i++) {
|
||||
modal = modal.replaceAll(`Port${i}Check`, details.ports[i]?.check || '');
|
||||
modal = modal.replaceAll(`Port${i}External`, details.ports[i]?.external || '');
|
||||
modal = modal.replaceAll(`Port${i}Internal`, details.ports[i]?.internal || '');
|
||||
modal = modal.replaceAll(`Port${i}Protocol`, details.ports[i]?.protocol || '');
|
||||
}
|
||||
|
||||
for (let i = 0; i <= 6; i++) {
|
||||
modal = modal.replaceAll(`Vol${i}Source`, details.volumes[i]?.Source || '');
|
||||
modal = modal.replaceAll(`Vol${i}Destination`, details.volumes[i]?.Destination || '');
|
||||
modal = modal.replaceAll(`Vol${i}RW`, details.volumes[i]?.RW || '');
|
||||
}
|
||||
|
||||
|
||||
for (let i = 0; i <= 19; i++) {
|
||||
modal = modal.replaceAll(`Label${i}Key`, Object.keys(details.labels)[i] || '');
|
||||
modal = modal.replaceAll(`Label${i}Value`, Object.values(details.labels)[i] || '');
|
||||
}
|
||||
|
||||
// console.log(details.env);
|
||||
for (let i = 0; i <= 19; i++) {
|
||||
modal = modal.replaceAll(`Env${i}Key`, details.env[i]?.split('=')[0] || '');
|
||||
modal = modal.replaceAll(`Env${i}Value`, details.env[i]?.split('=')[1] || '');
|
||||
}
|
||||
|
||||
res.send(modal);
|
||||
return;
|
||||
case 'updates':
|
||||
res.send(newCards);
|
||||
newCards = '';
|
||||
return;
|
||||
case 'card':
|
||||
await userCards(req.session);
|
||||
if (!req.session.container_list.find(c => c.container === name)) {
|
||||
res.send('');
|
||||
return;
|
||||
} else {
|
||||
let details = await containerInfo(name);
|
||||
let card = await createCard(details);
|
||||
res.send(card);
|
||||
return;
|
||||
}
|
||||
case 'logs':
|
||||
let logString = '';
|
||||
let options = { follow: true, stdout: true, stderr: false, timestamps: false };
|
||||
docker.getContainer(name).logs(options, function (err, stream) {
|
||||
if (err) { console.log(err); return; }
|
||||
const readableStream = Readable.from(stream);
|
||||
readableStream.on('data', function (chunk) {
|
||||
logString += chunk.toString('utf8');
|
||||
});
|
||||
readableStream.on('end', function () {
|
||||
res.send(`<pre>${logString}</pre>`);
|
||||
});
|
||||
});
|
||||
return;
|
||||
case 'hide':
|
||||
let user = req.session.user;
|
||||
let exists = await Permission.findOne({ where: {containerName: name, user: user}});
|
||||
if (!exists) { const newPermission = await Permission.create({ containerName: name, user: user, hide: true, userID: req.session.UUID}); }
|
||||
else { exists.update({ hide: true }); }
|
||||
hidden = await Permission.findAll({ where: {user: user, hide: true}}, { attributes: ['containerName'] });
|
||||
hidden = hidden.map((container) => container.containerName);
|
||||
res.send("ok");
|
||||
return;
|
||||
case 'reset':
|
||||
await Permission.update({ hide: false }, { where: { user: req.session.user } });
|
||||
res.send("ok");
|
||||
return;
|
||||
case 'alert':
|
||||
req.session.alert = '';
|
||||
res.send('');
|
||||
return;
|
||||
}
|
||||
|
||||
function status (state) {
|
||||
return(`<span class="text-yellow align-items-center lh-1"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
|
||||
${state}
|
||||
</span>`);
|
||||
}
|
||||
|
||||
// Container actions
|
||||
if ((action == 'start') && (value == 'stopped')) {
|
||||
docker.getContainer(name).start();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'start') && (value == 'paused')) {
|
||||
docker.getContainer(name).unpause();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'stop') && (value != 'stopped')) {
|
||||
docker.getContainer(name).stop();
|
||||
res.send(status('stopping'));
|
||||
} else if ((action == 'pause') && (value == 'paused')) {
|
||||
docker.getContainer(name).unpause();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'pause') && (value == 'running')) {
|
||||
docker.getContainer(name).pause();
|
||||
res.send(status('pausing'));
|
||||
} else if (action == 'restart') {
|
||||
docker.getContainer(name).restart();
|
||||
res.send(status('restarting'));
|
||||
}
|
||||
}
|
||||
|
||||
async function containerInfo (containerName) {
|
||||
// get the container info
|
||||
let container = docker.getContainer(containerName);
|
||||
let info = await container.inspect();
|
||||
let image = info.Config.Image;
|
||||
// grab the service name from the end of the image name
|
||||
let service = image.split('/').pop();
|
||||
// remove the tag from the service name if it exists
|
||||
try { service = service.split(':')[0]; } catch {}
|
||||
let ports_list = [];
|
||||
let external = 0;
|
||||
let internal = 0;
|
||||
|
||||
try {
|
||||
for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
|
||||
let ports = {
|
||||
check: 'checked',
|
||||
external: value[0].HostPort,
|
||||
internal: key.split('/')[0],
|
||||
protocol: key.split('/')[1]
|
||||
}
|
||||
ports_list.push(ports);
|
||||
}
|
||||
} catch {}
|
||||
try {
|
||||
external = ports_list[0].external;
|
||||
internal = ports_list[0].internal;
|
||||
} catch {}
|
||||
|
||||
// console.log(ports_list);
|
||||
// console.log(info.HostConfig.PortBindings);
|
||||
|
||||
// console.log(info.HostConfig.Binds);
|
||||
|
||||
// console.log(info.Config.Env);
|
||||
// console.log(info.Config.Labels);
|
||||
|
||||
let details = {
|
||||
name: containerName,
|
||||
image: image,
|
||||
service: service,
|
||||
state: info.State.Status,
|
||||
external_port: external,
|
||||
internal_port: internal,
|
||||
ports: ports_list,
|
||||
volumes: info.Mounts,
|
||||
env: info.Config.Env,
|
||||
labels: info.Config.Labels,
|
||||
link: 'localhost',
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
async function createCard (details) {
|
||||
let shortname = details.name.slice(0, 10) + '...';
|
||||
let trigger = 'data-hx-trigger="load, every 3s"';
|
||||
let state = details.state;
|
||||
let card = readFileSync('./views/partials/containerFull.html', 'utf8');
|
||||
|
||||
let state_color = '';
|
||||
switch (state) {
|
||||
case 'running':
|
||||
state_color = 'green';
|
||||
break;
|
||||
case 'exited':
|
||||
state = 'stopped';
|
||||
state_color = 'red';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
case 'paused':
|
||||
state_color = 'orange';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
case 'installing':
|
||||
state_color = 'blue';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
}
|
||||
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
|
||||
|
||||
card = card.replace(/AppName/g, details.name);
|
||||
card = card.replace(/AppShortName/g, shortname);
|
||||
card = card.replace(/AppIcon/g, details.service);
|
||||
card = card.replace(/AppState/g, state);
|
||||
card = card.replace(/StateColor/g, state_color);
|
||||
card = card.replace(/ExternalPort/g, details.external_port);
|
||||
card = card.replace(/InternalPort/g, details.internal_port);
|
||||
card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
|
||||
card = card.replace(/AppNameState/g, `${details.name}State`);
|
||||
card = card.replace(/data-trigger=""/, trigger);
|
||||
return card;
|
||||
}
|
||||
|
||||
async function userCards (session) {
|
||||
session.container_list = [];
|
||||
// check what containers the user wants hidden
|
||||
let hidden = await Permission.findAll({ where: {user: session.user, hide: true}}, { attributes: ['containerName'] });
|
||||
hidden = hidden.map((container) => container.containerName);
|
||||
// check what containers the user has permission to view
|
||||
let visable = await Permission.findAll({ where: { user: session.user, [Op.or]: [{ uninstall: true }, { edit: true }, { upgrade: true }, { start: true }, { stop: true }, { pause: true }, { restart: true }, { logs: true }, { view: true }] } });
|
||||
visable = visable.map((container) => container.containerName);
|
||||
// get all containers
|
||||
let containers = await docker.listContainers({ all: true });
|
||||
// loop through containers
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
let container_name = containers[i].Names[0].replace('/', '');
|
||||
// skip hidden containers
|
||||
if (hidden.includes(container_name)) { continue; }
|
||||
// admin can see all containers that they don't have hidden
|
||||
if (session.role == 'admin') { session.container_list.push({ container: container_name, state: containers[i].State }); }
|
||||
// user can see any containers that they have any permissions for
|
||||
else if (visable.includes(container_name)){ session.container_list.push({ container: container_name, state: containers[i].State }); }
|
||||
}
|
||||
// create a sent list if it doesn't exist
|
||||
if (!session.sent_list) { session.sent_list = []; }
|
||||
if (!session.update_list) { session.update_list = []; }
|
||||
if (!session.new_cards) { session.new_cards = []; }
|
||||
}
|
||||
|
||||
async function updateDashboard (session) {
|
||||
let container_list = session.container_list;
|
||||
let sent_list = session.sent_list;
|
||||
session.new_cards = [];
|
||||
session.update_list = [];
|
||||
// loop through the containers list
|
||||
container_list.forEach(info => {
|
||||
let { container, state } = info;
|
||||
let sent = sent_list.find(c => c.container === container);
|
||||
if (!sent) { session.new_cards.push(container);}
|
||||
else if (sent.state !== state) { session.update_list.push(container); }
|
||||
});
|
||||
// loop through the sent list to see if any containers have been removed
|
||||
sent_list.forEach(info => {
|
||||
let { container } = info;
|
||||
let exists = container_list.find(c => c.container === container);
|
||||
if (!exists) { session.update_list.push(container); }
|
||||
});
|
||||
}
|
||||
|
||||
// HTMX server-side events
|
||||
export const SSE = async (req, res) => {
|
||||
// set the headers for server-sent events
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
||||
// check for container changes every 500ms
|
||||
let eventCheck = setInterval(async () => {
|
||||
await userCards(req.session);
|
||||
// check if the cards displayed are the same as what's in the session
|
||||
if ((JSON.stringify(req.session.container_list) === JSON.stringify(req.session.sent_list))) { return; }
|
||||
await updateDashboard(req.session);
|
||||
|
||||
for (let i = 0; i < req.session.new_cards.length; i++) {
|
||||
let details = await containerInfo(req.session.new_cards[i]);
|
||||
let card = await createCard(details);
|
||||
newCards += card;
|
||||
req.session.alert = '';
|
||||
}
|
||||
for (let i = 0; i < req.session.update_list.length; i++) {
|
||||
res.write(`event: ${req.session.update_list[i]}\n`);
|
||||
res.write(`data: 'update cards'\n\n`);
|
||||
}
|
||||
res.write(`event: update\n`);
|
||||
res.write(`data: 'update cards'\n\n`);
|
||||
req.session.sent_list = req.session.container_list.slice();
|
||||
}, 500);
|
||||
req.on('close', () => {
|
||||
clearInterval(eventCheck);
|
||||
});
|
||||
};
|
||||
|
||||
// Server metrics (CPU, RAM, TX, RX, DISK)
|
||||
export const Stats = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let color = req.header('hx-trigger');
|
||||
let value = 0;
|
||||
switch (name) {
|
||||
case 'CPU':
|
||||
await currentLoad().then(data => { value = Math.round(data.currentLoad); });
|
||||
break;
|
||||
case 'RAM':
|
||||
await mem().then(data => { value = Math.round((data.active / data.total) * 100); });
|
||||
break;
|
||||
case 'NET':
|
||||
let [down, up, percent] = [0, 0, 0];
|
||||
await networkStats().then(data => { down = Math.round(data[0].rx_bytes / (1024 * 1024)); up = Math.round(data[0].tx_bytes / (1024 * 1024)); percent = Math.round((down / 1000) * 100); });
|
||||
let net = `<div class="font-weight-medium"><label class="cpu-text mb-1">Down:${down}MB Up:${up}MB</label></div>
|
||||
<div class="cpu-bar meter animate ${color}"><span style="width:20%"><span></span></span></div>`;
|
||||
res.send(net);
|
||||
return;
|
||||
case 'DISK':
|
||||
await fsSize().then(data => { value = data[0].use; });
|
||||
break;
|
||||
}
|
||||
let info = `<div class="font-weight-medium"> <label class="cpu-text mb-1">${name} ${value}%</label></div>
|
||||
<div class="cpu-bar meter animate ${color}"> <span style="width:${value}%"><span></span></span> </div>`;
|
||||
res.send(info);
|
||||
}
|
||||
|
||||
// Imported by utils/install.js
|
||||
export async function addAlert (session, type, message) {
|
||||
session.alert = `<div class="alert alert-${type} alert-dismissible py-2 mb-0" role="alert" id="alert">
|
||||
<div class="d-flex">
|
||||
<div class="spinner-border text-info nav-link">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div>
|
||||
${message}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-close" data-hx-post="/dashboard/alert" data-hx-trigger="click" data-hx-target="#alert" data-hx-swap="outerHTML" style="padding-top: 0.5rem;" ></button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export const UpdatePermissions = async (req, res) => {
|
||||
let { user, container, reset_permissions } = req.body;
|
||||
let id = req.header('hx-trigger');
|
||||
if (reset_permissions) {
|
||||
await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false, view: false }, { where: { containerName: container} });
|
||||
return;
|
||||
}
|
||||
await Permission.update({ uninstall: false, edit: false, upgrade: false, start: false, stop: false, pause: false, restart: false, logs: false }, { where: { containerName: container, user: user } });
|
||||
Object.keys(req.body).forEach(async function(key) {
|
||||
if (key != 'user' && key != 'container') {
|
||||
let permissions = req.body[key];
|
||||
if (permissions.includes('uninstall')) { await Permission.update({ uninstall: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('edit')) { await Permission.update({ edit: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('upgrade')) { await Permission.update({ upgrade: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('start')) { await Permission.update({ start: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('stop')) { await Permission.update({ stop: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('pause')) { await Permission.update({ pause: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('restart')) { await Permission.update({ restart: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('logs')) { await Permission.update({ logs: true }, { where: {containerName: container, user: user}}); }
|
||||
if (permissions.includes('view')) { await Permission.update({ view: true }, { where: {containerName: container, user: user}}); }
|
||||
}
|
||||
});
|
||||
if (id == 'submit') {
|
||||
res.send('<button class="btn" type="button" id="confirmed" hx-post="/updatePermissions" hx-swap="outerHTML" hx-trigger="load delay:2s">Update ✔️</button>');
|
||||
return;
|
||||
} else if (id == 'confirmed') {
|
||||
res.send('<button class="btn" type="button" id="submit" hx-post="/updatePermissions" hx-vals="#updatePermissions" hx-swap="outerHTML">Update </button>');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Container charts
|
||||
export const Chart = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
if (!stats[name]) { stats[name] = { cpuArray: Array(15).fill(0), ramArray: Array(15).fill(0) }; }
|
||||
const info = await dockerContainerStats(name);
|
||||
stats[name].cpuArray.push(Math.round(info[0].cpuPercent));
|
||||
stats[name].ramArray.push(Math.round(info[0].memPercent));
|
||||
stats[name].cpuArray = stats[name].cpuArray.slice(-15);
|
||||
stats[name].ramArray = stats[name].ramArray.slice(-15);
|
||||
let chart = `
|
||||
<script>
|
||||
${name}chart.updateSeries([{
|
||||
data: [${stats[name].cpuArray}]
|
||||
}, {
|
||||
data: [${stats[name].ramArray}]
|
||||
}])
|
||||
</script>`
|
||||
res.send(chart);
|
||||
}
|
|
@ -1,20 +1,63 @@
|
|||
import { docker } from '../server.js';
|
||||
import { addAlert } from './dashboard.js';
|
||||
|
||||
export const Images = async function(req, res) {
|
||||
|
||||
let action = req.params.action;
|
||||
|
||||
if (action == "remove") {
|
||||
let images = req.body.select;
|
||||
|
||||
if (typeof(images) == 'string') {
|
||||
images = [images];
|
||||
}
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
if (images[i] != 'on') {
|
||||
try {
|
||||
console.log(`Removing image: ${images[i]}`);
|
||||
let image = docker.getImage(images[i]);
|
||||
await image.remove();
|
||||
} catch (error) {
|
||||
console.log(`Unable to remove image: ${images[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.redirect("/images");
|
||||
return;
|
||||
} else if (action == "add") {
|
||||
let image = req.body.image;
|
||||
let tag = req.body.tag || 'latest';
|
||||
|
||||
try {
|
||||
console.log(`Pulling image: ${image}:${tag}`);
|
||||
await docker.pull(`${image}:${tag}`);
|
||||
} catch (error) {
|
||||
console.log(`Unable to pull image: ${image}:${tag}`);
|
||||
}
|
||||
res.redirect("/images");
|
||||
return;
|
||||
}
|
||||
|
||||
let containers = await docker.listContainers({ all: true });
|
||||
let container_images = [];
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
container_images.push(containers[i].Image);
|
||||
}
|
||||
|
||||
let images = await docker.listImages({ all: true });
|
||||
|
||||
let image_list = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
|
||||
<th><button class="table-sort" data-sort="sort-name">Name</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-city">ID</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-type">Tag</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-progress">Action</button></th>
|
||||
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-type">Tag</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-tbody">`
|
||||
|
@ -22,22 +65,32 @@ export const Images = async function(req, res) {
|
|||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
|
||||
let name = '';
|
||||
let tag = '';
|
||||
try { name = images[i].RepoTags[0].split(':')[0]; } catch {}
|
||||
try { tag = images[i].RepoTags[0].split(':')[1]; } catch {}
|
||||
|
||||
let date = new Date(images[i].Created * 1000);
|
||||
let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
||||
|
||||
let size = images[i].Size / 1000 / 1000; // to match docker desktop
|
||||
size = size.toFixed(2);
|
||||
|
||||
let status = '';
|
||||
if (container_images.includes(images[i].RepoTags[0])) {
|
||||
status = 'In use';
|
||||
}
|
||||
|
||||
let details = `
|
||||
<tr>
|
||||
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
|
||||
<td class="sort-name">${images[i].RepoTags}</td>
|
||||
<td><input class="form-check-input m-0 align-middle" name="select" value="${images[i].Id}" type="checkbox" aria-label="Select"></td>
|
||||
<td class="sort-name">${name}</td>
|
||||
<td class="sort-type">${tag}</td>
|
||||
<td class="sort-city">${images[i].Id}</td>
|
||||
<td class="sort-type">Latest</td>
|
||||
<td class="sort-score text-green">In use</td>
|
||||
<td class="sort-score text-green">${status}</td>
|
||||
<td class="sort-date" data-date="1628122643">${created}</td>
|
||||
<td class="sort-quantity">${size} MB</td>
|
||||
<td class="text-end"><a class="btn" href="#">Details</a></td>
|
||||
<td class="text-end"><a class="btn" href="#"><svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg></a></td>
|
||||
</tr>`
|
||||
image_list += details;
|
||||
}
|
||||
|
@ -48,9 +101,10 @@ export const Images = async function(req, res) {
|
|||
res.render("images", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
image_list: image_list,
|
||||
image_count: images.length
|
||||
image_count: images.length,
|
||||
alert: '',
|
||||
});
|
||||
|
||||
}
|
|
@ -1,29 +1,23 @@
|
|||
import { User, Syslog } from '../database/models.js';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
|
||||
export const Login = function(req,res){
|
||||
if(req.session.user){
|
||||
res.redirect("/logout");
|
||||
}else{
|
||||
res.render("login",{
|
||||
"error":"",
|
||||
});
|
||||
}
|
||||
if (req.session.user) { res.redirect("/logout"); }
|
||||
else { res.render("login",{ "error":"", }); }
|
||||
}
|
||||
|
||||
export const submitLogin = async function(req,res){
|
||||
|
||||
let { email, password } = req.body;
|
||||
email = email.toLowerCase();
|
||||
|
||||
if(email && password){
|
||||
|
||||
if (email && password) {
|
||||
let existingUser = await User.findOne({ where: {email:email}});
|
||||
if(existingUser){
|
||||
if (existingUser) {
|
||||
|
||||
let match = await bcrypt.compare(password,existingUser.password);
|
||||
|
||||
if(match){
|
||||
|
||||
if (match) {
|
||||
let currentDate = new Date();
|
||||
let newLogin = currentDate.toLocaleString();
|
||||
await User.update({lastLogin: newLogin}, {where: {UUID:existingUser.UUID}});
|
||||
|
@ -40,14 +34,9 @@ export const submitLogin = async function(req,res){
|
|||
message: "User logged in successfully",
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
|
||||
if (req.session.role == "admin") {
|
||||
res.redirect("/");
|
||||
}
|
||||
else {
|
||||
res.redirect("/portal");
|
||||
}
|
||||
}else{
|
||||
|
||||
res.redirect("/dashboard");
|
||||
} else {
|
||||
|
||||
const syslog = await Syslog.create({
|
||||
user: null,
|
||||
|
@ -61,12 +50,12 @@ export const submitLogin = async function(req,res){
|
|||
"error":"Invalid password",
|
||||
});
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
res.render("login",{
|
||||
"error":"User with that email does not exist.",
|
||||
});
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
res.status(400);
|
||||
res.render("login",{
|
||||
"error":"Please fill in all the fields.",
|
||||
|
|
|
@ -3,35 +3,48 @@ import { docker } from '../server.js';
|
|||
|
||||
export const Networks = async function(req, res) {
|
||||
|
||||
|
||||
let container_networks = [];
|
||||
// List all containers
|
||||
let containers = await docker.listContainers({ all: true });
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
let network_name = containers[i].HostConfig.NetworkMode;
|
||||
try { container_networks.push(containers[i].NetworkSettings.Networks[network_name].NetworkID) } catch {}
|
||||
}
|
||||
|
||||
|
||||
let networks = await docker.listNetworks({ all: true });
|
||||
|
||||
let network_list = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
|
||||
<th><button class="table-sort" data-sort="sort-name">Name</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-city">ID</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-progress">Action</button></th>
|
||||
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-city">ID</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-tbody">`
|
||||
<tbody class="table-tbody">`
|
||||
|
||||
|
||||
for (let i = 0; i < networks.length; i++) {
|
||||
|
||||
// let date = new Date(images[i].Created * 1000);
|
||||
// let created = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
|
||||
|
||||
|
||||
|
||||
let status = '';
|
||||
if (container_networks.includes(networks[i].Id)) {
|
||||
status = `In use`;
|
||||
}
|
||||
|
||||
let details = `
|
||||
<tr>
|
||||
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
|
||||
<td><input class="form-check-input m-0 align-middle" name="select" value="${networks[i].Id}" type="checkbox" aria-label="Select"></td>
|
||||
<td class="sort-name">${networks[i].Name}</td>
|
||||
<td class="sort-city">${networks[i].Id}</td>
|
||||
<td class="sort-score text-green">In use</td>
|
||||
<td class="sort-score text-green">${status}</td>
|
||||
<td class="sort-date" data-date="1628122643">${networks[i].Created}</td>
|
||||
<td class="text-end"><a class="btn" href="#">Details</a></td>
|
||||
</tr>`
|
||||
|
@ -40,13 +53,37 @@ export const Networks = async function(req, res) {
|
|||
|
||||
network_list += `</tbody>`
|
||||
|
||||
|
||||
res.render("networks", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
network_list: network_list,
|
||||
network_count: networks.length
|
||||
network_count: networks.length,
|
||||
alert: '',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const removeNetwork = async function(req, res) {
|
||||
let networks = req.body.select;
|
||||
|
||||
if (typeof(networks) == 'string') {
|
||||
networks = [networks];
|
||||
}
|
||||
|
||||
for (let i = 0; i < networks.length; i++) {
|
||||
|
||||
if (networks[i] != 'on') {
|
||||
try {
|
||||
console.log(`Removing network: ${networks[i]}`);
|
||||
let network = docker.getNetwork(networks[i]);
|
||||
await network.remove();
|
||||
} catch (error) {
|
||||
console.log(`Unable to remove network: ${networks[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
res.redirect("/networks");
|
||||
}
|
|
@ -1,12 +1,389 @@
|
|||
import { Readable } from 'stream';
|
||||
import { Permission, Container, User } from '../database/models.js';
|
||||
import { docker } from '../server.js';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
let hidden = '';
|
||||
|
||||
// The actual page
|
||||
export const Portal = (req, res) => {
|
||||
|
||||
let name = req.session.user;
|
||||
let role = req.session.role;
|
||||
let avatar = name.charAt(0).toUpperCase();
|
||||
|
||||
res.render("portal", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
name: name,
|
||||
avatar: avatar,
|
||||
role: role,
|
||||
alert: '',
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function CardList () {
|
||||
let name = req.session.user;
|
||||
let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
let details = await containerInfo(containers[i].containerName);
|
||||
let card = await createCard(details);
|
||||
cardList += card;
|
||||
}
|
||||
}
|
||||
|
||||
export const UserContainers = async (req, res) => {
|
||||
let cardList = '';
|
||||
let name = req.session.user;
|
||||
let containers = await Permission.findAll({ attributes: ['containerName'], where: { user: name }});
|
||||
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
if (containers[i].containerName == null) { continue; }
|
||||
let details = await containerInfo(containers[i].containerName);
|
||||
let card = await createCard(details);
|
||||
cardList += card;
|
||||
}
|
||||
res.send(cardList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function containerInfo (containerName) {
|
||||
let container = docker.getContainer(containerName);
|
||||
let info = await container.inspect();
|
||||
let image = info.Config.Image.split('/');
|
||||
let ports_list = [];
|
||||
try {
|
||||
for (const [key, value] of Object.entries(info.HostConfig.PortBindings)) {
|
||||
let ports = {
|
||||
check: 'checked',
|
||||
external: value[0].HostPort,
|
||||
internal: key.split('/')[0],
|
||||
protocol: key.split('/')[1]
|
||||
}
|
||||
ports_list.push(ports);
|
||||
}
|
||||
} catch {
|
||||
// no exposed ports
|
||||
}
|
||||
|
||||
let external = 0;
|
||||
let internal = 0;
|
||||
try {
|
||||
external = ports_list[0].external;
|
||||
internal = ports_list[0].internal;
|
||||
} catch {
|
||||
// no exposed ports
|
||||
}
|
||||
|
||||
|
||||
let details = {
|
||||
name: containerName,
|
||||
image: image,
|
||||
service: image[image.length - 1].split(':')[0],
|
||||
state: info.State.Status,
|
||||
external_port: external,
|
||||
internal_port: internal,
|
||||
ports: ports_list,
|
||||
link: 'localhost',
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
async function createCard (details) {
|
||||
if (hidden.includes(details.name)) { return;}
|
||||
let shortname = details.name.slice(0, 10) + '...';
|
||||
let trigger = 'data-hx-trigger="load, every 3s"';
|
||||
let state = details.state;
|
||||
let state_color = '';
|
||||
switch (state) {
|
||||
case 'running':
|
||||
state_color = 'green';
|
||||
break;
|
||||
case 'exited':
|
||||
state = 'stopped';
|
||||
state_color = 'red';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
case 'paused':
|
||||
state_color = 'orange';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
case 'installing':
|
||||
state_color = 'blue';
|
||||
trigger = 'data-hx-trigger="load"';
|
||||
break;
|
||||
}
|
||||
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
|
||||
let card = readFileSync('./views/partials/containerSimple.html', 'utf8');
|
||||
card = card.replace(/AppName/g, details.name);
|
||||
card = card.replace(/AppShortName/g, shortname);
|
||||
card = card.replace(/AppIcon/g, details.service);
|
||||
card = card.replace(/AppState/g, state);
|
||||
card = card.replace(/StateColor/g, state_color);
|
||||
card = card.replace(/ExternalPort/g, details.external_port);
|
||||
card = card.replace(/InternalPort/g, details.internal_port);
|
||||
card = card.replace(/ChartName/g, details.name.replace(/-/g, ''));
|
||||
card = card.replace(/AppNameState/g, `${details.name}State`);
|
||||
card = card.replace(/data-trigger=""/, trigger);
|
||||
return card;
|
||||
}
|
||||
|
||||
|
||||
let [ cardList, newCards, containersArray, sentArray, updatesArray ] = [ '', '', [], [], [] ];
|
||||
|
||||
export async function addCard (name, state) {
|
||||
console.log(`Adding card for ${name}: ${state}`);
|
||||
|
||||
let details = {
|
||||
name: name,
|
||||
image: name,
|
||||
service: name,
|
||||
state: 'installing',
|
||||
external_port: 0,
|
||||
internal_port: 0,
|
||||
ports: [],
|
||||
link: 'localhost',
|
||||
|
||||
}
|
||||
createCard(details).then(card => {
|
||||
cardList += card;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// HTMX server-side events
|
||||
export const SSE = async (req, res) => {
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
|
||||
|
||||
let eventCheck = setInterval(async () => {
|
||||
// builds array of containers and their states
|
||||
containersArray = [];
|
||||
await docker.listContainers({ all: true }).then(containers => {
|
||||
containers.forEach(container => {
|
||||
let name = container.Names[0].replace('/', '');
|
||||
if (!hidden.includes(name)) { // if not hidden
|
||||
containersArray.push({ container: name, state: container.State });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if ((JSON.stringify(containersArray) !== JSON.stringify(sentArray))) {
|
||||
cardList = '';
|
||||
newCards = '';
|
||||
containersArray.forEach(container => {
|
||||
const { container: containerName, state } = container;
|
||||
const existingContainer = sentArray.find(c => c.container === containerName);
|
||||
if (!existingContainer) {
|
||||
containerInfo(containerName).then(details => {
|
||||
createCard(details).then(card => {
|
||||
newCards += card;
|
||||
});
|
||||
});
|
||||
res.write(`event: update\n`);
|
||||
res.write(`data: 'update cards'\n\n`);
|
||||
} else if (existingContainer.state !== state) {
|
||||
updatesArray.push(containerName);
|
||||
}
|
||||
containerInfo(containerName).then(details => {
|
||||
createCard(details).then(card => {
|
||||
cardList += card;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
sentArray.forEach(container => {
|
||||
const { container: containerName } = container;
|
||||
const existingContainer = containersArray.find(c => c.container === containerName);
|
||||
if (!existingContainer) {
|
||||
updatesArray.push(containerName);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0; i < updatesArray.length; i++) {
|
||||
res.write(`event: ${updatesArray[i]}\n`);
|
||||
res.write(`data: 'update cards'\n\n`);
|
||||
}
|
||||
updatesArray = [];
|
||||
sentArray = containersArray.slice();
|
||||
}
|
||||
|
||||
}, 500);
|
||||
|
||||
|
||||
req.on('close', () => {
|
||||
clearInterval(eventCheck);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const updateCards = async (req, res) => {
|
||||
console.log('updateCards called');
|
||||
res.send(newCards);
|
||||
newCards = '';
|
||||
}
|
||||
|
||||
|
||||
export const Containers = async (req, res) => {
|
||||
CardList();
|
||||
// res.send(cardList);
|
||||
}
|
||||
|
||||
export const Card = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
console.log(`${name} requesting updated card`);
|
||||
// return nothing if in hidden or not found in containersArray
|
||||
if (hidden.includes(name) || !containersArray.find(c => c.container === name)) {
|
||||
res.send('');
|
||||
return;
|
||||
} else {
|
||||
let details = await containerInfo(name);
|
||||
let card = await createCard(details);
|
||||
res.send(card);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function status (state) {
|
||||
let status = `<span class="text-yellow align-items-center lh-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
|
||||
${state}
|
||||
</span>`;
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
export const Logs = (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
function containerLogs (data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let logString = '';
|
||||
var options = { follow: false, stdout: true, stderr: false, timestamps: false };
|
||||
var containerName = docker.getContainer(data);
|
||||
containerName.logs(options, function (err, stream) {
|
||||
if (err) { reject(err); return; }
|
||||
const readableStream = Readable.from(stream);
|
||||
readableStream.on('data', function (chunk) {
|
||||
logString += chunk.toString('utf8');
|
||||
});
|
||||
readableStream.on('end', function () {
|
||||
resolve(logString);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
containerLogs(name).then((data) => {
|
||||
res.send(`<pre>${data}</pre> `)
|
||||
});
|
||||
}
|
||||
|
||||
export const Action = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let state = req.header('hx-trigger');
|
||||
let action = req.params.action;
|
||||
// Start
|
||||
if ((action == 'start') && (state == 'stopped')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.start();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'start') && (state == 'paused')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.unpause();
|
||||
res.send(status('starting'));
|
||||
// Stop
|
||||
} else if ((action == 'stop') && (state != 'stopped')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.stop();
|
||||
res.send(status('stopping'));
|
||||
// Pause
|
||||
} else if ((action == 'pause') && (state == 'paused')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.unpause();
|
||||
res.send(status('starting'));
|
||||
} else if ((action == 'pause') && (state == 'running')) {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.pause();
|
||||
res.send(status('pausing'));
|
||||
// Restart
|
||||
} else if (action == 'restart') {
|
||||
var containerName = docker.getContainer(name);
|
||||
containerName.restart();
|
||||
res.send(status('restarting'));
|
||||
// Hide
|
||||
} else if (action == 'hide') {
|
||||
let exists = await Container.findOne({ where: {name: name}});
|
||||
if (!exists) {
|
||||
const newContainer = await Container.create({ name: name, visibility: false, });
|
||||
} else {
|
||||
exists.update({ visibility: false });
|
||||
}
|
||||
hidden = await Container.findAll({ where: {visibility:false}});
|
||||
hidden = hidden.map((container) => container.name);
|
||||
res.send("ok");
|
||||
// Reset View
|
||||
} else if (action == 'reset') {
|
||||
await Container.update({ visibility: true }, { where: {} });
|
||||
hidden = await Container.findAll({ where: {visibility:false}});
|
||||
hidden = hidden.map((container) => container.name);
|
||||
res.send("ok");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const Modals = async (req, res) => {
|
||||
let name = req.header('hx-trigger-name');
|
||||
let id = req.header('hx-trigger');
|
||||
let title = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
if (id == 'permissions') {
|
||||
let permissions_list = '';
|
||||
let permissions_modal = readFileSync('./views/modals/permissions.html', 'utf8');
|
||||
permissions_modal = permissions_modal.replace(/PermissionsTitle/g, title);
|
||||
let users = await User.findAll({ attributes: ['username', 'UUID']});
|
||||
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
let user_permissions = readFileSync('./views/partials/user_permissions.html', 'utf8');
|
||||
let exists = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
|
||||
if (!exists) {
|
||||
const newPermission = await Permission.create({ containerName: name, user: users[i].username, userID: users[i].UUID});
|
||||
}
|
||||
|
||||
let permissions = await Permission.findOne({ where: {containerName: name, user: users[i].username}});
|
||||
if (permissions.uninstall == true) { user_permissions = user_permissions.replace(/data-UninstallCheck/g, 'checked'); }
|
||||
if (permissions.edit == true) { user_permissions = user_permissions.replace(/data-EditCheck/g, 'checked'); }
|
||||
if (permissions.upgrade == true) { user_permissions = user_permissions.replace(/data-UpgradeCheck/g, 'checked'); }
|
||||
if (permissions.start == true) { user_permissions = user_permissions.replace(/data-StartCheck/g, 'checked'); }
|
||||
if (permissions.stop == true) { user_permissions = user_permissions.replace(/data-StopCheck/g, 'checked'); }
|
||||
if (permissions.pause == true) { user_permissions = user_permissions.replace(/data-PauseCheck/g, 'checked'); }
|
||||
if (permissions.restart == true) { user_permissions = user_permissions.replace(/data-RestartCheck/g, 'checked'); }
|
||||
if (permissions.logs == true) { user_permissions = user_permissions.replace(/data-LogsCheck/g, 'checked'); }
|
||||
|
||||
user_permissions = user_permissions.replace(/EntryNumber/g, i);
|
||||
user_permissions = user_permissions.replace(/PermissionsUsername/g, users[i].username);
|
||||
user_permissions = user_permissions.replace(/PermissionsContainer/g, name);
|
||||
|
||||
permissions_list += user_permissions;
|
||||
}
|
||||
|
||||
permissions_modal = permissions_modal.replace(/PermissionsList/g, permissions_list);
|
||||
res.send(permissions_modal);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (id == 'uninstall') {
|
||||
let modal = readFileSync('./views/modals/uninstall.html', 'utf8');
|
||||
modal = modal.replace(/AppName/g, name);
|
||||
// let containerPermissions = await Permission.findAll({ where: {containerName: name}});
|
||||
res.send(modal);
|
||||
return;
|
||||
}
|
||||
|
||||
let modal = readFileSync('./views/modals/details.html', 'utf8');
|
||||
let details = await containerInfo(name);
|
||||
|
||||
modal = modal.replace(/AppName/g, details.name);
|
||||
modal = modal.replace(/AppImage/g, details.image);
|
||||
res.send(modal);
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { User, Syslog } from '../database/models.js';
|
||||
import { User, Syslog, Permission } from '../database/models.js';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
let SECRET = process.env.SECRET || "MrWiskers"
|
||||
|
@ -17,7 +17,8 @@ export const Register = function(req,res){
|
|||
|
||||
export const submitRegister = async function(req,res){
|
||||
|
||||
let { name, username, email, password, confirmPassword, avatar, warning, secret } = req.body;
|
||||
let { name, username, email, password, confirmPassword, secret } = req.body;
|
||||
email = email.toLowerCase();
|
||||
|
||||
|
||||
if (secret != SECRET) {
|
||||
|
@ -30,7 +31,7 @@ export const submitRegister = async function(req,res){
|
|||
});
|
||||
}
|
||||
|
||||
if((name && email && password && confirmPassword && username && warning) && (secret == SECRET) && (password == confirmPassword)){
|
||||
if((name && email && password && confirmPassword && username) && (secret == SECRET) && (password == confirmPassword)){
|
||||
|
||||
async function userRole () {
|
||||
let userCount = await User.count();
|
||||
|
@ -55,7 +56,6 @@ export const submitRegister = async function(req,res){
|
|||
password: bcrypt.hashSync(password,10),
|
||||
role: await userRole(),
|
||||
group: 'all',
|
||||
avatar: `<img src="img/avatars/${avatar}">`,
|
||||
lastLogin: newLogin,
|
||||
});
|
||||
|
||||
|
@ -67,7 +67,11 @@ export const submitRegister = async function(req,res){
|
|||
req.session.user = newUser.username;
|
||||
req.session.UUID = newUser.UUID;
|
||||
req.session.role = newUser.role;
|
||||
req.session.avatar = newUser.avatar;
|
||||
|
||||
const permission = await Permission.create({
|
||||
user: newUser.username,
|
||||
userID: newUser.UUID
|
||||
});
|
||||
|
||||
const syslog = await Syslog.create({
|
||||
user: req.session.user,
|
||||
|
@ -77,7 +81,7 @@ export const submitRegister = async function(req,res){
|
|||
ip: req.socket.remoteAddress
|
||||
});
|
||||
|
||||
res.redirect("/");
|
||||
res.redirect("/dashboard");
|
||||
}
|
||||
} catch(err) {
|
||||
res.render("register",{
|
||||
|
@ -94,7 +98,7 @@ export const submitRegister = async function(req,res){
|
|||
} else {
|
||||
// Redirect to the signup page.
|
||||
res.render("register",{
|
||||
"error":"Please fill in all the fields and acknowledge security warning.",
|
||||
"error":"Please fill in all the fields.",
|
||||
});
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ export const Settings = (req, res) => {
|
|||
res.render("settings", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
alert: '',
|
||||
});
|
||||
}
|
31
controllers/supporters.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { User } from "../database/models.js";
|
||||
|
||||
export const Supporters = async (req, res) => {
|
||||
|
||||
let user = await User.findOne({ where: { UUID: req.session.UUID }});
|
||||
|
||||
|
||||
res.render("supporters", {
|
||||
first_name: user.name,
|
||||
last_name: user.name,
|
||||
name: user.name,
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
alert: '',
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
let thanks = 0;
|
||||
export const Thanks = async (req, res) => {
|
||||
thanks++;
|
||||
let data = thanks.toString();
|
||||
if (thanks > 999) {
|
||||
data = 'Did you really click 1000 times?!';
|
||||
}
|
||||
res.send(data);
|
||||
}
|
|
@ -29,8 +29,9 @@ export const Syslogs = async function(req, res) {
|
|||
res.render("syslogs", {
|
||||
name: req.session.user || 'Dev',
|
||||
role: req.session.role || 'Dev',
|
||||
avatar: req.session.avatar || '<img src="/img/avatars/rus.jpg">',
|
||||
logs: logs
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
logs: logs,
|
||||
alert: '',
|
||||
});
|
||||
|
||||
}
|
|
@ -20,13 +20,14 @@ export const Users = async (req, res) => {
|
|||
let allUsers = await User.findAll();
|
||||
allUsers.forEach((account) => {
|
||||
|
||||
let active = '<span class="badge badge-outline text-green">Active</span>'
|
||||
let active = '<span class="badge badge-outline text-green" title="User has logged-in within the last 30 days.">Active</span>'
|
||||
let lastLogin = new Date(account.lastLogin);
|
||||
let currentDate = new Date();
|
||||
let days = Math.floor((currentDate - lastLogin) / (1000 * 60 * 60 * 24));
|
||||
let avatar = account.username.charAt(0);
|
||||
|
||||
if (days > 30) {
|
||||
active = '<span class="badge badge-outline text-grey">Inactive</span>';
|
||||
active = '<span class="badge badge-outline text-grey" title="User has not logged-in within the last 30 days.">Inactive</span>';
|
||||
}
|
||||
|
||||
|
||||
|
@ -35,7 +36,7 @@ export const Users = async (req, res) => {
|
|||
<tr>
|
||||
<td><input class="form-check-input" type="checkbox"></td>
|
||||
<td>${account.id}</td>
|
||||
<td><span class="avatar me-2">${account.avatar}</span></td>
|
||||
<td><span class="avatar avatar-sm bg-green-lt">${avatar}</span></span>
|
||||
<td>${account.name}</td>
|
||||
<td>${account.username}</td>
|
||||
<td>${account.email}</td>
|
||||
|
@ -43,7 +44,7 @@ export const Users = async (req, res) => {
|
|||
<td>${account.role}</td>
|
||||
<td>${account.lastLogin}</td>
|
||||
<td>${active}</td>
|
||||
<td><a href="#" class="btn">Edit</a></td>
|
||||
<td><a href="#" class="btn">View</a></td>
|
||||
</tr>`
|
||||
|
||||
user_list += info;
|
||||
|
@ -53,8 +54,9 @@ export const Users = async (req, res) => {
|
|||
res.render("users", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
user_list: user_list
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
user_list: user_list,
|
||||
alert: ''
|
||||
});
|
||||
|
||||
}
|
9
controllers/variables.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
|
||||
export const Variables = (req, res) => {
|
||||
|
||||
res.render("variables", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
});
|
||||
}
|
|
@ -1,59 +1,66 @@
|
|||
import { docker } from '../server.js';
|
||||
|
||||
|
||||
export const Volumes = async function(req, res) {
|
||||
let container_volumes = [];
|
||||
let volume_list = '';
|
||||
|
||||
// Table header
|
||||
volume_list = `<thead>
|
||||
<tr>
|
||||
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
|
||||
<th><label class="table-sort" data-sort="sort-type">Type</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-name">Name</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-city">Mount point</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-score">Status</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-date">Created</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-quantity">Size</label></th>
|
||||
<th><label class="table-sort" data-sort="sort-progress">Action</label></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-tbody">`
|
||||
|
||||
// List all containers
|
||||
let containers = await docker.listContainers({ all: true });
|
||||
|
||||
// Get the first 6 volumes from each container
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
try { container_volumes.push({type: containers[i].Mounts[0].Type, source: containers[i].Mounts[0].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[1].Type, source: containers[i].Mounts[1].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[2].Type, source: containers[i].Mounts[2].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[3].Type, source: containers[i].Mounts[3].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[4].Type, source: containers[i].Mounts[4].Source}); } catch { }
|
||||
try { container_volumes.push({type: containers[i].Mounts[5].Type, source: containers[i].Mounts[5].Source}); } catch { }
|
||||
}
|
||||
|
||||
// List ALL volumes
|
||||
let list = await docker.listVolumes({ all: true });
|
||||
let volumes = list.Volumes;
|
||||
|
||||
let volume_list = `
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-1"><input class="form-check-input m-0 align-middle" name="select" type="checkbox" aria-label="Select all" onclick="selectAll()"></th>
|
||||
<th><button class="table-sort" data-sort="sort-name">Name</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-city">Mount point</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-score">Status</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-date">Created</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-quantity">Size</button></th>
|
||||
<th><button class="table-sort" data-sort="sort-progress">Action</button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table-tbody">`
|
||||
|
||||
|
||||
|
||||
// Create a table row for each volume
|
||||
for (let i = 0; i < volumes.length; i++) {
|
||||
let volume = volumes[i];
|
||||
let name = volume.Name;
|
||||
let mount = volume.Mountpoint;
|
||||
let name = "" + volume.Name;
|
||||
let mount = "" + volume.Mountpoint;
|
||||
let type = "Bind";
|
||||
|
||||
if (name.length > 40) {
|
||||
name = name.slice(0, 37) + '...';
|
||||
}
|
||||
// Check if the volume is being used by any of the containers
|
||||
let status = '';
|
||||
if (container_volumes.some(volume => volume.source === mount)) { status = "In use"; }
|
||||
if (container_volumes.some(volume => volume.source === mount && volume.type === 'volume')) { type = "Volume"; }
|
||||
|
||||
if (mount.length > 70) {
|
||||
mount = mount.slice(0, 67) + '...';
|
||||
}
|
||||
|
||||
// docker.df(volume.Mountpoint).then((data) => {
|
||||
// for (let key in data) {
|
||||
// console.log(data[key]);
|
||||
// }
|
||||
// });
|
||||
|
||||
|
||||
let details = `
|
||||
let row = `
|
||||
<tr>
|
||||
<td><input class="form-check-input m-0 align-middle" name="select" value="" type="checkbox" aria-label="Select"></td>
|
||||
<td><input class="form-check-input m-0 align-middle" name="select" value="${name}" type="checkbox" aria-label="Select"></td>
|
||||
<td class="sort-type">${type}</td>
|
||||
<td class="sort-name">${name}</td>
|
||||
<td class="sort-city">${mount}</td>
|
||||
<td class="sort-score text-green">In use</td>
|
||||
<td class="sort-score text-green">${status}</td>
|
||||
<td class="sort-date" data-date="1628122643">${volume.CreatedAt}</td>
|
||||
<td class="sort-quantity">MB</td>
|
||||
<td class="text-end"><a class="btn" href="#">Details</a></td>
|
||||
</tr>`
|
||||
|
||||
volume_list += details;
|
||||
volume_list += row;
|
||||
}
|
||||
|
||||
volume_list += `</tbody>`
|
||||
|
@ -62,9 +69,51 @@ export const Volumes = async function(req, res) {
|
|||
res.render("volumes", {
|
||||
name: req.session.user,
|
||||
role: req.session.role,
|
||||
avatar: req.session.avatar,
|
||||
avatar: req.session.user.charAt(0).toUpperCase(),
|
||||
volume_list: volume_list,
|
||||
volume_count: volumes.length
|
||||
volume_count: volumes.length,
|
||||
alert: '',
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const addVolume = async function(req, res) {
|
||||
|
||||
let volume = req.body.volume;
|
||||
|
||||
docker.createVolume({
|
||||
Name: volume
|
||||
});
|
||||
res.redirect("/volumes");
|
||||
}
|
||||
|
||||
|
||||
export const removeVolume = async function(req, res) {
|
||||
let volumes = req.body.select;
|
||||
|
||||
if (typeof(volumes) == 'string') {
|
||||
volumes = [volumes];
|
||||
}
|
||||
|
||||
for (let i = 0; i < volumes.length; i++) {
|
||||
|
||||
if (volumes[i] != 'on') {
|
||||
try {
|
||||
console.log(`Removing volume: ${volumes[i]}`);
|
||||
let volume = docker.getVolume(volumes[i]);
|
||||
await volume.remove();
|
||||
} catch (error) {
|
||||
console.log(`Unable to remove volume: ${volumes[i]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.redirect("/volumes");
|
||||
}
|
||||
|
||||
|
||||
// docker.df(volume.Name).then((data) => {
|
||||
// for (let key in data) {
|
||||
// console.log(data[key]);
|
||||
// }
|
||||
// });
|
||||
|
|
|
@ -1,17 +1,9 @@
|
|||
import { Sequelize, DataTypes } from 'sequelize';
|
||||
|
||||
// let SQLITE_PASS = process.env.SQLITE_PASS || 'some_long_elaborate_password';
|
||||
|
||||
// export const sequelize = new Sequelize('dweebui', 'dweebui', SQLITE_PASS, {
|
||||
// dialect: 'sqlite',
|
||||
// dialectModulePath: '@journeyapps/sqlcipher',
|
||||
// storage: './database/database.sqlite',
|
||||
// logging: false,
|
||||
// });
|
||||
|
||||
export const sequelize = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: './database/database.sqlite',
|
||||
storage: './database/db.sqlite',
|
||||
logging: false,
|
||||
});
|
||||
|
||||
|
@ -55,25 +47,60 @@ export const User = sequelize.define('User', {
|
|||
});
|
||||
|
||||
export const Container = sequelize.define('Container', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
visibility: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
size: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
group: {
|
||||
type: DataTypes.STRING
|
||||
}
|
||||
});
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
visibility: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
service: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
state: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
image: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
external_port: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
internal_port: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
ports: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
volumes: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
environment_variables: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
labels: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
IPv4: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
style: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
cpu: {
|
||||
// store the last 15 values from dockerContainerStats
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
ram: {
|
||||
// store the last 15 values from dockerContainerStats
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
});
|
||||
|
||||
export const Permission = sequelize.define('Permission', {
|
||||
id: {
|
||||
|
@ -83,11 +110,9 @@ export const Permission = sequelize.define('Permission', {
|
|||
},
|
||||
containerName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
containerID: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
user: {
|
||||
type: DataTypes.STRING,
|
||||
|
@ -99,39 +124,51 @@ export const Permission = sequelize.define('Permission', {
|
|||
},
|
||||
install: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
uninstall: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
edit: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
upgrade: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
start: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
stop: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
restart: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
pause: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
logs: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
hide: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
view: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
reset_view: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
view: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: false
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -180,10 +217,40 @@ export const Notification = sequelize.define('Notification', {
|
|||
color: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
read: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
createdAt : {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
createdBy : {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export const Settings = sequelize.define('Settings', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true
|
||||
},
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const Variables = sequelize.define('Variables', {
|
||||
find: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
replace: {
|
||||
type: DataTypes.STRING,
|
||||
}
|
||||
});
|
|
@ -1,208 +0,0 @@
|
|||
import { writeFileSync, mkdirSync, readFileSync } from "fs";
|
||||
import yaml from 'js-yaml';
|
||||
import { execSync } from "child_process";
|
||||
import { docker } from "../server.js";
|
||||
import DockerodeCompose from "dockerode-compose";
|
||||
import { Syslog } from "../database/models.js";
|
||||
import { containerCard } from "../components/containerCard.js";
|
||||
|
||||
// This entire page hurts to look at.
|
||||
export const Install = async (req, res) => {
|
||||
|
||||
console.log(req.app.locals.installCard);
|
||||
|
||||
let data = req.body;
|
||||
|
||||
let { service_name, name, image, command_check, command, net_mode, restart_policy } = data;
|
||||
let { port0, port1, port2, port3, port4, port5 } = data;
|
||||
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
|
||||
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
|
||||
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
|
||||
|
||||
let ports = [port0, port1, port2, port3, port4, port5]
|
||||
|
||||
let docker_volumes = [];
|
||||
|
||||
if (image.startsWith('https://')){
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
|
||||
console.log(`Downloaded stackfile: ${image}`);
|
||||
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
|
||||
let services = Object.keys(stackfile.services);
|
||||
|
||||
for ( let i = 0; i < services.length; i++ ) {
|
||||
try {
|
||||
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
|
||||
} catch { console.log('no env') }
|
||||
}
|
||||
} else {
|
||||
|
||||
let compose_file = `version: '3'`;
|
||||
compose_file += `\nservices:`
|
||||
compose_file += `\n ${service_name}:`
|
||||
compose_file += `\n container_name: ${name}`;
|
||||
compose_file += `\n image: ${image}`;
|
||||
|
||||
// Command
|
||||
if (command_check == 'on') {
|
||||
compose_file += `\n command: ${command}`
|
||||
}
|
||||
|
||||
// Network mode
|
||||
if (net_mode == 'host') {
|
||||
compose_file += `\n network_mode: 'host'`
|
||||
}
|
||||
else if (net_mode != 'host' && net_mode != 'docker') {
|
||||
compose_file += `\n network_mode: '${net_mode}'`
|
||||
}
|
||||
|
||||
// Restart policy
|
||||
if (restart_policy != '') {
|
||||
compose_file += `\n restart: ${restart_policy}`
|
||||
}
|
||||
|
||||
// Ports
|
||||
for (let i = 0; i < ports.length; i++) {
|
||||
if ((ports[i] == 'on') && (net_mode != 'host')) {
|
||||
compose_file += `\n ports:`
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < ports.length; i++) {
|
||||
if ((ports[i] == 'on') && (net_mode != 'host')) {
|
||||
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Volumes
|
||||
let volumes = [volume0, volume1, volume2, volume3, volume4, volume5]
|
||||
|
||||
for (let i = 0; i < volumes.length; i++) {
|
||||
if (volumes[i] == 'on') {
|
||||
compose_file += `\n volumes:`
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < volumes.length; i++) {
|
||||
|
||||
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
|
||||
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
|
||||
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
}
|
||||
|
||||
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
|
||||
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
|
||||
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
|
||||
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
docker_volumes.push(`${name}_${volume_name}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11]
|
||||
|
||||
for (let i = 0; i < env_vars.length; i++) {
|
||||
if (env_vars[i] == 'on') {
|
||||
compose_file += `\n environment:`
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < env_vars.length; i++) {
|
||||
if (env_vars[i] == 'on') {
|
||||
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
|
||||
}
|
||||
}
|
||||
|
||||
// Labels
|
||||
let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11]
|
||||
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
if (labels[i] == 'on') {
|
||||
compose_file += `\n labels:`
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`label${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
|
||||
}
|
||||
}
|
||||
|
||||
// Privileged mode
|
||||
if (data.privileged == 'on') {
|
||||
compose_file += `\n privileged: true`
|
||||
}
|
||||
|
||||
// Hardware acceleration
|
||||
for (let i = 0; i < env_vars.length; i++) {
|
||||
if ((env_vars[i] == 'on') && (data[`env_${i}_name`] == 'DRINODE')) {
|
||||
compose_file += `\n deploy:`
|
||||
compose_file += `\n resources:`
|
||||
compose_file += `\n reservations:`
|
||||
compose_file += `\n devices:`
|
||||
compose_file += `\n - driver: nvidia`
|
||||
compose_file += `\n count: 1`
|
||||
compose_file += `\n capabilities: [gpu]`
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// add any docker volumes to the docker-compose file
|
||||
if ( docker_volumes.length > 0 ) {
|
||||
compose_file += `\n`
|
||||
compose_file += `\nvolumes:`
|
||||
|
||||
// check docker_volumes for duplicates and remove them completely
|
||||
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
|
||||
|
||||
for (let i = 0; i < docker_volumes.length; i++) {
|
||||
if ( docker_volumes[i] != '') {
|
||||
compose_file += `\n ${docker_volumes[i]}:`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
|
||||
|
||||
} catch { console.log('error creating directory or compose file') }
|
||||
|
||||
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await compose.pull();
|
||||
await compose.up().then(() => {
|
||||
const syslog = Syslog.create({
|
||||
user: req.session.user,
|
||||
email: null,
|
||||
event: "App Installation",
|
||||
message: `${name} installed successfully`,
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const syslog = await Syslog.create({
|
||||
user: req.session.user,
|
||||
email: null,
|
||||
event: "App Installation",
|
||||
message: `${name} installation failed: ${err}`,
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
|
||||
res.redirect('/');
|
||||
|
||||
}
|
||||
|
||||
|
2955
package-lock.json
generated
38
package.json
|
@ -1,37 +1,29 @@
|
|||
{
|
||||
"name": "dweebui",
|
||||
"version": "1.0.0",
|
||||
"description": "A web UI for Docker",
|
||||
"version": "0.60",
|
||||
"description": "Free and Open-Source WebUI For Managing Your Containers.",
|
||||
"main": "server.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "mocha --require @babel/register"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"author": "lllllllillllllillll",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/register": "^7.23.7",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"adm-zip": "^0.5.12",
|
||||
"bcrypt": "^5.1.1",
|
||||
"chai": "^5.0.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "^4.0.2",
|
||||
"dockerode-compose": "^1.4.0",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-session": "^1.17.3",
|
||||
"helmet": "^7.1.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mocha": "^10.2.0",
|
||||
"sequelize": "^6.35.2",
|
||||
"sinon": "^17.0.1",
|
||||
"socket.io": "^4.7.4",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"express-session": "^1.18.0",
|
||||
"memorystore": "^1.6.7",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sequelize": "^6.37.3",
|
||||
"sqlite3": "^5.1.7",
|
||||
"stream": "^0.0.2",
|
||||
"supertest": "^6.3.3",
|
||||
"systeminformation": "^5.21.22"
|
||||
"systeminformation": "^5.22.9",
|
||||
"yaml": "^2.4.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,6 +83,10 @@
|
|||
.blue > span {
|
||||
background-image: linear-gradient(#2478f5, #22017e);
|
||||
}
|
||||
|
||||
.purple > span {
|
||||
background-image: linear-gradient(#bd14d3, #670370);
|
||||
}
|
||||
|
||||
.nostripes > span > span,
|
||||
.nostripes > span::after {
|
||||
|
|
10
public/css/tabler.min.css
vendored
|
@ -6036,7 +6036,7 @@ fieldset:disabled .btn {
|
|||
color: var(--tblr-alert-color);
|
||||
background-color: var(--tblr-alert-bg);
|
||||
border: var(--tblr-alert-border);
|
||||
border-radius: var(--tblr-alert-border-radius)
|
||||
border-radius: var(--tblr-alert-border-radius);
|
||||
}
|
||||
|
||||
.alert-heading {
|
||||
|
@ -20221,11 +20221,11 @@ body[data-bs-theme=dark] .hide-theme-dark {
|
|||
}
|
||||
|
||||
.alert {
|
||||
--tblr-alert-color: var(--tblr-muted);
|
||||
background: #fff;
|
||||
--tblr-alert-color: var(--tblr-secondary);
|
||||
--tblr-alert-bg: var(--tblr-surface);
|
||||
border: var(--tblr-border-width) var(--tblr-border-style) var(--tblr-border-color-translucent);
|
||||
border-left: .25rem var(--tblr-border-style) var(--tblr-alert-color);
|
||||
box-shadow: rgba(24, 36, 51, .04) 0 2px 4px 0
|
||||
border-left: 0.25rem var(--tblr-border-style) var(--tblr-alert-color);
|
||||
box-shadow: rgba(24, 36, 51, 0.04) 0 2px 4px 0;
|
||||
}
|
||||
|
||||
.alert>:last-child {
|
||||
|
|
BIN
public/img/add to zip.jpg
Normal file
After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 69 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 413 B After Width: | Height: | Size: 413 B |
BIN
public/img/logo.png
Normal file
After Width: | Height: | Size: 19 KiB |
355
public/js/htmx-sse.js
Normal file
|
@ -0,0 +1,355 @@
|
|||
/*
|
||||
Server Sent Events Extension
|
||||
============================
|
||||
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
|
||||
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
/** @type {import("../htmx").HtmxInternalApi} */
|
||||
var api;
|
||||
|
||||
htmx.defineExtension("sse", {
|
||||
|
||||
/**
|
||||
* Init saves the provided reference to the internal HTMX API.
|
||||
*
|
||||
* @param {import("../htmx").HtmxInternalApi} api
|
||||
* @returns void
|
||||
*/
|
||||
init: function(apiRef) {
|
||||
// store a reference to the internal API.
|
||||
api = apiRef;
|
||||
|
||||
// set a function in the public API for creating new EventSource objects
|
||||
if (htmx.createEventSource == undefined) {
|
||||
htmx.createEventSource = createEventSource;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* onEvent handles all events passed to this extension.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Event} evt
|
||||
* @returns void
|
||||
*/
|
||||
onEvent: function(name, evt) {
|
||||
|
||||
switch (name) {
|
||||
|
||||
case "htmx:beforeCleanupElement":
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
// Try to remove remove an EventSource when elements are removed
|
||||
if (internalData.sseEventSource) {
|
||||
internalData.sseEventSource.close();
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
// Try to create EventSources when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
ensureEventSourceOnElement(evt.target);
|
||||
registerSSE(evt.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// HELPER FUNCTIONS
|
||||
///////////////////////////////////////////////
|
||||
|
||||
|
||||
/**
|
||||
* createEventSource is the default method for creating new EventSource objects.
|
||||
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns EventSource
|
||||
*/
|
||||
function createEventSource(url) {
|
||||
return new EventSource(url, { withCredentials: true });
|
||||
}
|
||||
|
||||
function splitOnWhitespace(trigger) {
|
||||
return trigger.trim().split(/\s+/);
|
||||
}
|
||||
|
||||
function getLegacySSEURL(elt) {
|
||||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
||||
if (legacySSEValue) {
|
||||
var values = splitOnWhitespace(legacySSEValue);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i].split(/:(.+)/);
|
||||
if (value[0] === "connect") {
|
||||
return value[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLegacySSESwaps(elt) {
|
||||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
||||
var returnArr = [];
|
||||
if (legacySSEValue != null) {
|
||||
var values = splitOnWhitespace(legacySSEValue);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i].split(/:(.+)/);
|
||||
if (value[0] === "swap") {
|
||||
returnArr.push(value[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return returnArr;
|
||||
}
|
||||
|
||||
/**
|
||||
* registerSSE looks for attributes that can contain sse events, right
|
||||
* now hx-trigger and sse-swap and adds listeners based on these attributes too
|
||||
* the closest event source
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
*/
|
||||
function registerSSE(elt) {
|
||||
// Find closest existing event source
|
||||
var sourceElement = api.getClosestMatch(elt, hasEventSource);
|
||||
if (sourceElement == null) {
|
||||
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
||||
return null; // no eventsource in parentage, orphaned element
|
||||
}
|
||||
|
||||
// Set internalData and source
|
||||
var internalData = api.getInternalData(sourceElement);
|
||||
var source = internalData.sseEventSource;
|
||||
|
||||
// Add message handlers for every `sse-swap` attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
|
||||
|
||||
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
|
||||
if (sseSwapAttr) {
|
||||
var sseEventNames = sseSwapAttr.split(",");
|
||||
} else {
|
||||
var sseEventNames = getLegacySSESwaps(child);
|
||||
}
|
||||
|
||||
for (var i = 0; i < sseEventNames.length; i++) {
|
||||
var sseEventName = sseEventNames[i].trim();
|
||||
var listener = function(event) {
|
||||
|
||||
// If the source is missing then close SSE
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the body no longer contains the element, remove the listener
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
|
||||
// swap the response into the DOM and trigger a notification
|
||||
swap(child, event.data);
|
||||
api.triggerEvent(elt, "htmx:sseMessage", event);
|
||||
};
|
||||
|
||||
// Register the new listener
|
||||
api.getInternalData(child).sseEventListener = listener;
|
||||
source.addEventListener(sseEventName, listener);
|
||||
}
|
||||
});
|
||||
|
||||
// Add message handlers for every `hx-trigger="sse:*"` attribute
|
||||
queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
|
||||
|
||||
var sseEventName = api.getAttributeValue(child, "hx-trigger");
|
||||
if (sseEventName == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process hx-triggers for events with the "sse:" prefix
|
||||
if (sseEventName.slice(0, 4) != "sse:") {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the sse: prefix from here on out
|
||||
sseEventName = sseEventName.substr(4);
|
||||
|
||||
var listener = function() {
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ensureEventSourceOnElement creates a new EventSource connection on the provided element.
|
||||
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
||||
* is created and stored in the element's internalData.
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} retryCount
|
||||
* @returns {EventSource | null}
|
||||
*/
|
||||
function ensureEventSourceOnElement(elt, retryCount) {
|
||||
|
||||
if (elt == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// handle extension source creation attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
|
||||
var sseURL = api.getAttributeValue(child, "sse-connect");
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
// handle legacy sse, remove for HTMX2
|
||||
queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
|
||||
var sseURL = getLegacySSEURL(child);
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function ensureEventSource(elt, url, retryCount) {
|
||||
var source = htmx.createEventSource(url);
|
||||
|
||||
source.onerror = function(err) {
|
||||
|
||||
// Log an error event
|
||||
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
|
||||
|
||||
// If parent no longer exists in the document, then clean up this EventSource
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, try to reconnect the EventSource
|
||||
if (source.readyState === EventSource.CLOSED) {
|
||||
retryCount = retryCount || 0;
|
||||
var timeout = Math.random() * (2 ^ retryCount) * 500;
|
||||
window.setTimeout(function() {
|
||||
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
source.onopen = function(evt) {
|
||||
api.triggerEvent(elt, "htmx:sseOpen", { source: source });
|
||||
}
|
||||
|
||||
api.getInternalData(elt).sseEventSource = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* maybeCloseSSESource confirms that the parent element still exists.
|
||||
* If not, then any associated SSE source is closed and the function returns true.
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @returns boolean
|
||||
*/
|
||||
function maybeCloseSSESource(elt) {
|
||||
if (!api.bodyContains(elt)) {
|
||||
var source = api.getInternalData(elt).sseEventSource;
|
||||
if (source != undefined) {
|
||||
source.close();
|
||||
// source = null
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} attributeName
|
||||
*/
|
||||
function queryAttributeOnThisOrChildren(elt, attributeName) {
|
||||
|
||||
var result = [];
|
||||
|
||||
// If the parent element also contains the requested attribute, then add it to the results too.
|
||||
if (api.hasAttribute(elt, attributeName)) {
|
||||
result.push(elt);
|
||||
}
|
||||
|
||||
// Search all child nodes that match the requested attribute
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
|
||||
result.push(node);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} content
|
||||
*/
|
||||
function swap(elt, content) {
|
||||
|
||||
api.withExtensions(elt, function(extension) {
|
||||
content = extension.transformResponse(content, null, elt);
|
||||
});
|
||||
|
||||
var swapSpec = api.getSwapSpecification(elt);
|
||||
var target = api.getTarget(elt);
|
||||
var settleInfo = api.makeSettleInfo(elt);
|
||||
|
||||
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
|
||||
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.add(htmx.config.settlingClass);
|
||||
}
|
||||
api.triggerEvent(elt, 'htmx:beforeSettle');
|
||||
});
|
||||
|
||||
// Handle settle tasks (with delay if requested)
|
||||
if (swapSpec.settleDelay > 0) {
|
||||
setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
|
||||
} else {
|
||||
doSettle(settleInfo)();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* doSettle mirrors much of the functionality in htmx that
|
||||
* settles elements after their content has been swapped.
|
||||
* TODO: this should be published by htmx, and not duplicated here
|
||||
* @param {import("../htmx").HtmxSettleInfo} settleInfo
|
||||
* @returns () => void
|
||||
*/
|
||||
function doSettle(settleInfo) {
|
||||
|
||||
return function() {
|
||||
settleInfo.tasks.forEach(function(task) {
|
||||
task.call();
|
||||
});
|
||||
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.remove(htmx.config.settlingClass);
|
||||
}
|
||||
api.triggerEvent(elt, 'htmx:afterSettle');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hasEventSource(node) {
|
||||
return api.getInternalData(node).sseEventSource != null;
|
||||
}
|
||||
|
||||
})();
|
1
public/js/htmx.min.js
vendored
Normal file
|
@ -1,141 +0,0 @@
|
|||
socket.on('connect', () => {
|
||||
console.log('connected');
|
||||
//clear localStorage (because of code in old versions)
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
// Server metrics
|
||||
const cpuText = document.getElementById('cpu-text');
|
||||
const cpuBar = document.getElementById('cpu-bar');
|
||||
const ramText = document.getElementById('ram-text');
|
||||
const ramBar = document.getElementById('ram-bar');
|
||||
const netText = document.getElementById('net-text');
|
||||
const netBar = document.getElementById('net-bar');
|
||||
const diskText = document.getElementById('disk-text');
|
||||
const diskBar = document.getElementById('disk-bar');
|
||||
|
||||
// Container cards
|
||||
const dockerCards = document.getElementById('cards');
|
||||
|
||||
// Container logs
|
||||
const logViewer = document.getElementById('logView');
|
||||
|
||||
// Server metrics
|
||||
socket.on('metrics', (data) => {
|
||||
let [cpu, ram, tx, rx, disk] = data;
|
||||
|
||||
cpuText.innerHTML = `<span>CPU ${cpu} %</span>`;
|
||||
if (cpu < 7 ) { cpu = 7; }
|
||||
cpuBar.innerHTML = `<span style="width: ${cpu}%"><span></span></span>`;
|
||||
|
||||
ramText.innerHTML = `<span>RAM ${ram} %</span>`;
|
||||
if (ram < 7 ) { ram = 7; }
|
||||
ramBar.innerHTML = `<span style="width: ${ram}%"><span></span></span>`;
|
||||
|
||||
tx = Math.round(tx / 1024 / 1024);
|
||||
rx = Math.round(rx / 1024 / 1024);
|
||||
|
||||
netText.innerHTML = `<span>Down: ${rx}MB</span><span> Up: ${tx}MB</span>`;
|
||||
netBar.innerHTML = `<span style="width: 50%"><span></span></span>`;
|
||||
|
||||
diskText.innerHTML = `<span>DISK ${disk} %</span>`;
|
||||
if (disk < 7 ) { disk = 7; }
|
||||
diskBar.innerHTML = `<span style="width: ${disk}%"><span></span></span>`;
|
||||
});
|
||||
|
||||
// Container cards
|
||||
socket.on('containers', (data) => {
|
||||
let deleteMeElements = document.querySelectorAll('.deleteme');
|
||||
deleteMeElements.forEach((element) => {
|
||||
element.parentNode.removeChild(element);
|
||||
});
|
||||
dockerCards.insertAdjacentHTML("afterend", data);
|
||||
});
|
||||
|
||||
|
||||
function drawCharts(name, cpuArray, ramArray) {
|
||||
let element = document.querySelector(`${name}`);
|
||||
|
||||
let chart = new ApexCharts(element, {
|
||||
chart: {
|
||||
type: "line",
|
||||
height: 40.0,
|
||||
sparkline: {
|
||||
enabled: true
|
||||
},
|
||||
animations: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
},
|
||||
stroke: {
|
||||
width: [2, 1],
|
||||
dashArray: [0, 3],
|
||||
lineCap: "round",
|
||||
curve: "smooth"
|
||||
},
|
||||
series: [{
|
||||
name: "CPU",
|
||||
data: cpuArray
|
||||
}, {
|
||||
name: "RAM",
|
||||
data: ramArray
|
||||
}],
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 4
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
padding: 0
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
padding: 4
|
||||
}
|
||||
},
|
||||
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
})
|
||||
chart.render();
|
||||
}
|
||||
|
||||
// Buttons functions
|
||||
function clicked(button) {
|
||||
socket.emit('clicked', {name: button.name, id: button.id, value: button.value});
|
||||
}
|
||||
|
||||
|
||||
socket.on('containerStats', (data) => {
|
||||
let containerStats = data;
|
||||
|
||||
for (const [name, statsArray] of Object.entries(containerStats)) {
|
||||
|
||||
let cpuArray = statsArray.cpuArray;
|
||||
let ramArray = statsArray.ramArray;
|
||||
|
||||
let chart = document.getElementById(`${name}_chart`);
|
||||
if (chart) {
|
||||
chart.innerHTML = '';
|
||||
drawCharts(`#${name}_chart`, cpuArray, ramArray);
|
||||
} else {
|
||||
console.log(`Chart element with id ${name}_chart not found in the DOM`);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
socket.on('logs', (data) => {
|
||||
logViewer.innerHTML = `<pre>${data}</pre>`;
|
||||
});
|
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB |
107
router/index.js
|
@ -1,61 +1,106 @@
|
|||
import express from "express";
|
||||
import { Permission } from '../database/models.js';
|
||||
export const router = express.Router();
|
||||
|
||||
// Controllers
|
||||
import { Login, submitLogin, Logout } from "../controllers/login.js";
|
||||
import { Register, submitRegister } from "../controllers/register.js";
|
||||
import { Dashboard, searchDashboard } from "../controllers/dashboard.js";
|
||||
import { Apps, appSearch } from "../controllers/apps.js";
|
||||
import { Dashboard, DashboardAction, Stats, Chart, SSE, UpdatePermissions } from "../controllers/dashboard.js";
|
||||
import { Apps, appSearch, InstallModal, ImportModal, LearnMore, Upload, removeTemplate } from "../controllers/apps.js";
|
||||
import { Users } from "../controllers/users.js";
|
||||
import { Images } from "../controllers/images.js";
|
||||
import { Networks, removeNetwork } from "../controllers/networks.js";
|
||||
import { Volumes, addVolume, removeVolume } from "../controllers/volumes.js";
|
||||
import { Account } from "../controllers/account.js";
|
||||
import { Variables } from "../controllers/variables.js";
|
||||
import { Settings } from "../controllers/settings.js";
|
||||
import { Networks } from "../controllers/networks.js";
|
||||
import { Volumes } from "../controllers/volumes.js";
|
||||
import { Supporters, Thanks } from "../controllers/supporters.js";
|
||||
import { Syslogs } from "../controllers/syslogs.js";
|
||||
import { Portal } from "../controllers/portal.js"
|
||||
import { Install } from "../utils/install.js"
|
||||
import { Uninstall } from "../utils/uninstall.js"
|
||||
|
||||
/// Functions
|
||||
import { Install } from "../functions/install.js"
|
||||
import { Uninstall } from "../functions/uninstall.js"
|
||||
// Permission Middleware
|
||||
const adminOnly = async (req, res, next) => {
|
||||
if (req.session.role == 'admin') { next(); }
|
||||
else { res.redirect('/dashboard'); }
|
||||
}
|
||||
|
||||
// Auth middleware
|
||||
const auth = (req, res, next) => {
|
||||
if (req.session.role == "admin") {
|
||||
const sessionCheck = async (req, res, next) => {
|
||||
if (req.session.user) { next(); }
|
||||
else { res.redirect('/login'); }
|
||||
}
|
||||
|
||||
const permissionCheck = async (req, res, next) => {
|
||||
if (req.session.role == 'admin') { next(); return; }
|
||||
let user = req.session.user;
|
||||
let action = req.path.split("/")[2];
|
||||
let trigger = req.header('hx-trigger-name');
|
||||
const userAction = ['start', 'stop', 'restart', 'pause', 'uninstall', 'upgrade', 'edit', 'logs', 'view'];
|
||||
const userPaths = ['card', 'updates', 'hide', 'reset', 'alert'];
|
||||
if (userAction.includes(action)) {
|
||||
let permission = await Permission.findOne({ where: { containerName: trigger, user: user }, attributes: [`${action}`] });
|
||||
if (permission) {
|
||||
if (permission[action] == true) {
|
||||
console.log(`User ${user} has permission to ${action} ${trigger}`);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
else {
|
||||
console.log(`User ${user} does not have permission to ${action} ${trigger}`);
|
||||
}
|
||||
}
|
||||
} else if (userPaths.includes(action)) {
|
||||
next();
|
||||
} else {
|
||||
res.redirect("/login");
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Utils
|
||||
router.post("/install", adminOnly, Install);
|
||||
router.post("/uninstall", adminOnly, Uninstall);
|
||||
|
||||
// Routes
|
||||
router.get("/login", Login);
|
||||
router.post("/login", submitLogin);
|
||||
router.get("/logout", Logout);
|
||||
|
||||
router.get("/register", Register);
|
||||
router.post("/register", submitRegister);
|
||||
|
||||
router.get("/", sessionCheck, Dashboard);
|
||||
router.get("/dashboard", sessionCheck, Dashboard);
|
||||
router.post("/dashboard/:action", sessionCheck, permissionCheck, DashboardAction);
|
||||
router.get("/sse", sessionCheck, SSE);
|
||||
router.post("/updatePermissions", adminOnly, UpdatePermissions);
|
||||
router.get("/stats", sessionCheck, Stats);
|
||||
router.get("/chart", sessionCheck, Chart);
|
||||
|
||||
router.get("/", auth, Dashboard);
|
||||
router.post("/", auth, searchDashboard);
|
||||
router.get("/images", adminOnly, Images);
|
||||
router.post("/images/:action", adminOnly, Images);
|
||||
|
||||
router.get("/images", auth, Images);
|
||||
router.get("/volumes", auth, Volumes);
|
||||
router.get("/networks", auth, Networks);
|
||||
router.get("/portal", Portal)
|
||||
router.get("/volumes", adminOnly, Volumes);
|
||||
router.post("/addVolume", adminOnly, addVolume);
|
||||
router.post("/removeVolume", adminOnly, removeVolume);
|
||||
|
||||
router.get("/apps", auth, Apps);
|
||||
router.get("/apps/:page", auth, Apps);
|
||||
router.post("/apps", auth, appSearch);
|
||||
router.get("/networks", adminOnly, Networks);
|
||||
router.post("/removeNetwork", adminOnly, removeNetwork);
|
||||
|
||||
router.get("/users", auth, Users);
|
||||
router.get("/syslogs", auth, Syslogs);
|
||||
router.get("/apps/:page?/:template?", adminOnly, Apps);
|
||||
router.post("/apps", adminOnly, appSearch);
|
||||
router.get("/remove_template/:template", adminOnly, removeTemplate);
|
||||
|
||||
router.get("/install_modal", adminOnly, InstallModal)
|
||||
router.get("/import_modal", adminOnly, ImportModal)
|
||||
router.get("/learn_more", adminOnly, LearnMore)
|
||||
router.post("/upload", adminOnly, Upload);
|
||||
|
||||
router.get("/users", adminOnly, Users);
|
||||
router.get("/syslogs", adminOnly, Syslogs);
|
||||
|
||||
router.get("/variables", adminOnly, Variables);
|
||||
router.get("/settings", adminOnly, Settings);
|
||||
|
||||
|
||||
router.get("/account", Account);
|
||||
router.get("/settings", auth, Settings);
|
||||
router.get("/account", sessionCheck, Account);
|
||||
router.get("/supporters", sessionCheck, Supporters);
|
||||
router.post("/thank", sessionCheck, Thanks);
|
||||
|
||||
// Functions
|
||||
router.post("/install", auth, Install);
|
||||
router.post("/uninstall", auth, Uninstall);
|
||||
|
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 166 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
347
server.js
|
@ -1,343 +1,48 @@
|
|||
import express from 'express';
|
||||
import session from 'express-session';
|
||||
import compression from 'compression';
|
||||
import helmet from 'helmet';
|
||||
import memorystore from 'memorystore';
|
||||
import ejs from 'ejs';
|
||||
import Docker from 'dockerode';
|
||||
import cors from 'cors';
|
||||
import { Readable } from 'stream';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
import { instrument } from '@socket.io/admin-ui'
|
||||
import { router } from './router/index.js';
|
||||
import { createServer } from 'node:http';
|
||||
import { Server } from 'socket.io';
|
||||
import { sequelize, Container } from './database/models.js';
|
||||
import { currentLoad, mem, networkStats, fsSize, dockerContainerStats, dockerImages, networkInterfaces } from 'systeminformation';
|
||||
import { containerCard } from './components/containerCard.js';
|
||||
|
||||
export const app = express();
|
||||
const server = createServer(app);
|
||||
const port = process.env.PORT || 8000;
|
||||
export var docker = new Docker();
|
||||
let [cpu, ram, tx, rx, disk] = [0, 0, 0, 0, 0];
|
||||
let [hidden, clicked, dockerEvents] = ['', false, ''];
|
||||
let metricsInterval, cardsInterval, graphsInterval;
|
||||
let cardList = '';
|
||||
const statsArray = {};
|
||||
|
||||
// Socket.io admin ui
|
||||
export const io = new Server(server, {
|
||||
cors: {
|
||||
origin: ['http://localhost:8000', 'https://admin.socket.io'],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
}
|
||||
});
|
||||
instrument(io, {
|
||||
auth: false,
|
||||
readonly: true
|
||||
});
|
||||
import { sequelize } from './database/models.js';
|
||||
export const docker = new Docker();
|
||||
|
||||
// Session middleware
|
||||
const MemoryStore = memorystore(session);
|
||||
const sessionMiddleware = session({
|
||||
store: new MemoryStore({ checkPeriod: 86400000 }), // Prune expired entries every 24h
|
||||
secret: "keyboard cat",
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie:{
|
||||
secure:false, // Only set to true if you are using HTTPS.
|
||||
httpOnly:false, // Only set to true if you are using HTTPS.
|
||||
maxAge:3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
|
||||
secure: false,
|
||||
httpOnly: false,
|
||||
maxAge: 3600000 * 8 // Session max age in milliseconds. 3600000 = 1 hour.
|
||||
}
|
||||
});
|
||||
|
||||
// Make session data available to socket.io
|
||||
io.engine.use(sessionMiddleware);
|
||||
|
||||
// Rate limiter
|
||||
const limiter = rateLimit({
|
||||
windowMs: 5 * 60 * 1000, // 5 minutes
|
||||
limit: 50, // Limit each IP to 50 requests per `window`.
|
||||
standardHeaders: 'draft-7',
|
||||
legacyHeaders: false,
|
||||
})
|
||||
|
||||
// Express middleware
|
||||
app.set('view engine', 'ejs');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 8000;
|
||||
app.set('view engine', 'html');
|
||||
app.engine('html', ejs.renderFile);
|
||||
app.use([
|
||||
compression(),
|
||||
cors(),
|
||||
helmet({contentSecurityPolicy: false}),
|
||||
express.static("public"),
|
||||
express.json(),
|
||||
express.static('public'),
|
||||
express.urlencoded({ extended: true }),
|
||||
sessionMiddleware,
|
||||
router,
|
||||
limiter
|
||||
router
|
||||
]);
|
||||
|
||||
// Initialize server
|
||||
server.listen(port, async () => {
|
||||
async function init() {
|
||||
try { await sequelize.authenticate().then(() => { console.log('[Connected to DB]') }); }
|
||||
catch { console.log('[Could not connect to DB]'); }
|
||||
try { await sequelize.sync().then(() => { console.log('[Models Synced]') }); }
|
||||
catch { console.log('[Could not Sync Models]', error); }
|
||||
await getHidden();
|
||||
containerCards();
|
||||
}
|
||||
await init();
|
||||
app.emit("appStarted");
|
||||
console.log(`\nServer listening on http://localhost:${port}`);
|
||||
});
|
||||
|
||||
// Server metrics
|
||||
let serverMetrics = async () => {
|
||||
currentLoad().then(data => {
|
||||
cpu = Math.round(data.currentLoad);
|
||||
app.listen(PORT, async () => {
|
||||
async function init() {// I made sure the console.logs and emojis lined up
|
||||
try { await sequelize.authenticate().then(
|
||||
() => { console.log('DB Connection: ✔️') }); }
|
||||
catch { console.log('DB Connection: ❌'); }
|
||||
try { await sequelize.sync().then(
|
||||
() => { console.log('Synced Models: ✔️') }); }
|
||||
catch { console.log('Synced Models: ❌'); } }
|
||||
await init().then(() => {
|
||||
console.log(`Listening on http://localhost:${PORT}`);
|
||||
});
|
||||
mem().then(data => {
|
||||
ram = Math.round((data.active / data.total) * 100);
|
||||
});
|
||||
networkStats().then(data => {
|
||||
tx = data[0].tx_bytes / (1024 * 1024);
|
||||
rx = data[0].rx_bytes / (1024 * 1024);
|
||||
});
|
||||
fsSize().then(data => {
|
||||
disk = data[0].use;
|
||||
});
|
||||
}
|
||||
|
||||
// List docker containers
|
||||
let containerCards = async () => {
|
||||
let list = '';
|
||||
const allContainers = await docker.listContainers({ all: true });
|
||||
for (const container of allContainers) {
|
||||
if (!hidden.includes(container.Names[0].slice(1))) {
|
||||
|
||||
let imageVersion = container.Image.split('/');
|
||||
let service = imageVersion[imageVersion.length - 1].split(':')[0];
|
||||
let containerId = docker.getContainer(container.Id);
|
||||
let containerInfo = await containerId.inspect();
|
||||
let ports_list = [];
|
||||
try {
|
||||
for (const [key, value] of Object.entries(containerInfo.HostConfig.PortBindings)) {
|
||||
let ports = {
|
||||
check: 'checked',
|
||||
external: value[0].HostPort,
|
||||
internal: key.split('/')[0],
|
||||
protocol: key.split('/')[1]
|
||||
}
|
||||
ports_list.push(ports);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
let external_port = ports_list[0]?.external || 0;
|
||||
let internal_port = ports_list[0]?.internal || 0;
|
||||
|
||||
let container_info = {
|
||||
name: container.Names[0].slice(1),
|
||||
service: service,
|
||||
id: container.Id,
|
||||
state: container.State,
|
||||
image: container.Image,
|
||||
external_port: external_port,
|
||||
internal_port: internal_port,
|
||||
ports: ports_list,
|
||||
link: 'localhost',
|
||||
}
|
||||
let card = containerCard(container_info);
|
||||
list += card;
|
||||
}
|
||||
}
|
||||
cardList = list;
|
||||
}
|
||||
|
||||
// Container metrics
|
||||
let containerStats = async () => {
|
||||
const data = await docker.listContainers({ all: true });
|
||||
for (const container of data) {
|
||||
if (!hidden.includes(container.Names[0].slice(1))) {
|
||||
const stats = await dockerContainerStats(container.Id);
|
||||
const name = container.Names[0].slice(1);
|
||||
|
||||
if (!statsArray[name]) {
|
||||
statsArray[name] = {
|
||||
cpuArray: Array(15).fill(0),
|
||||
ramArray: Array(15).fill(0)
|
||||
};
|
||||
}
|
||||
statsArray[name].cpuArray.push(Math.round(stats[0].cpuPercent));
|
||||
statsArray[name].ramArray.push(Math.round(stats[0].memPercent));
|
||||
|
||||
statsArray[name].cpuArray = statsArray[name].cpuArray.slice(-15);
|
||||
statsArray[name].ramArray = statsArray[name].ramArray.slice(-15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store docker events
|
||||
docker.getEvents((err, stream) => {
|
||||
if (err) throw err;
|
||||
stream.on('data', (chunk) => {
|
||||
dockerEvents += chunk.toString('utf8');
|
||||
});
|
||||
});
|
||||
|
||||
// Check for docker events
|
||||
setInterval(async () => {
|
||||
if (dockerEvents != '') {
|
||||
await getHidden();
|
||||
await containerCards();
|
||||
dockerEvents = '';
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Get hidden containers
|
||||
async function getHidden() {
|
||||
hidden = await Container.findAll({ where: {visibility:false}});
|
||||
hidden = hidden.map((container) => container.name);
|
||||
}
|
||||
|
||||
// Socket.io
|
||||
io.on('connection', (socket) => {
|
||||
let sessionData = socket.request.session;
|
||||
let sent = '';
|
||||
if (sessionData.user != undefined) {
|
||||
console.log(`${sessionData.user} connected from ${socket.handshake.headers.host}`);
|
||||
|
||||
// Start intervals if not already started
|
||||
if (!metricsInterval) {
|
||||
serverMetrics();
|
||||
metricsInterval = setInterval(serverMetrics, 1000);
|
||||
console.log('Metrics interval started');
|
||||
}
|
||||
if (!cardsInterval) {
|
||||
containerCards();
|
||||
cardsInterval = setInterval(containerCards, 1000);
|
||||
console.log('Cards interval started');
|
||||
}
|
||||
if (!graphsInterval) {
|
||||
containerStats();
|
||||
graphsInterval = setInterval(containerStats, 1000);
|
||||
console.log('Graphs interval started');
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
socket.emit('metrics', [cpu, ram, tx, rx, disk]);
|
||||
if (sent != cardList) {
|
||||
sent = cardList;
|
||||
socket.emit('containers', cardList);
|
||||
}
|
||||
socket.emit('containerStats', statsArray);
|
||||
}, 1000);
|
||||
|
||||
|
||||
// Client input
|
||||
socket.on('clicked', (data) => {
|
||||
let { name, id, value } = data;
|
||||
console.log(`${sessionData.user} clicked: ${id} ${value} ${name}`);
|
||||
if (clicked == true) { return; } clicked = true;
|
||||
|
||||
// View container logs
|
||||
if (id == 'logs'){
|
||||
function containerLogs (data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let logString = '';
|
||||
var options = {
|
||||
follow: false,
|
||||
stdout: true,
|
||||
stderr: false,
|
||||
timestamps: false
|
||||
};
|
||||
var containerName = docker.getContainer(data);
|
||||
containerName.logs(options, function (err, stream) {
|
||||
if (err) { reject(err); return; }
|
||||
const readableStream = Readable.from(stream);
|
||||
readableStream.on('data', function (chunk) {
|
||||
logString += chunk.toString('utf8');
|
||||
});
|
||||
readableStream.on('end', function () {
|
||||
resolve(logString);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
containerLogs(name).then((data) => {
|
||||
socket.emit('logs', data);
|
||||
});
|
||||
}
|
||||
|
||||
// start, stop, pause, restart container
|
||||
if (id == 'start' || id == 'stop' || id == 'pause' || id == 'restart'){
|
||||
var containerName = docker.getContainer(name);
|
||||
|
||||
if ((id == 'start') && (value == 'stopped')) {
|
||||
containerName.start();
|
||||
} else if ((id == 'start') && (value == 'paused')) {
|
||||
containerName.unpause();
|
||||
} else if ((id == 'stop') && (value != 'stopped')) {
|
||||
containerName.stop();
|
||||
} else if ((id == 'pause') && (value == 'running')) {
|
||||
containerName.pause();
|
||||
} else if ((id == 'pause') && (value == 'paused')) {
|
||||
containerName.unpause();
|
||||
} else if (id == 'restart') {
|
||||
containerName.restart();
|
||||
}
|
||||
}
|
||||
|
||||
// hide container
|
||||
if (id == 'hide') {
|
||||
async function hideContainer() {
|
||||
let containerExists = await Container.findOne({ where: {name: name}});
|
||||
if(!containerExists) {
|
||||
const newContainer = await Container.create({ name: name, visibility: false, });
|
||||
getHidden();
|
||||
} else {
|
||||
containerExists.update({ visibility: false });
|
||||
getHidden();
|
||||
}
|
||||
|
||||
}
|
||||
hideContainer();
|
||||
}
|
||||
|
||||
// unhide containers
|
||||
if (id == 'resetView') {
|
||||
Container.update({ visibility: true }, { where: {} });
|
||||
getHidden();
|
||||
}
|
||||
|
||||
clicked = false;
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`${sessionData.user} disconnected`);
|
||||
socket.disconnect();
|
||||
// clear intervals if no users are connected
|
||||
if (io.engine.clientsCount == 0) {
|
||||
clearInterval(metricsInterval);
|
||||
clearInterval(cardsInterval);
|
||||
clearInterval(graphsInterval);
|
||||
metricsInterval = null;
|
||||
cardsInterval = null;
|
||||
graphsInterval = null;
|
||||
console.log('All intervals cleared');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('Missing session data');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// let link = '';
|
||||
// networkInterfaces().then(data => {
|
||||
// link = data[0].ip4;
|
||||
// });
|
||||
|
||||
});
|
2
templates/compose/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
|
@ -1128,7 +1128,7 @@
|
|||
}
|
||||
],
|
||||
"image": "linuxserver/minetest:latest",
|
||||
"logo": "https://raw.githubusercontent.com/linuxserver/beta-templates/master/lsiodev/img/minetest-icon.png",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/minetest.png",
|
||||
"platform": "linux",
|
||||
"ports": [
|
||||
"30000/udp"
|
||||
|
@ -1260,7 +1260,7 @@
|
|||
}
|
||||
],
|
||||
"image": "linuxserver/letsencrypt:latest",
|
||||
"logo": "https://raw.githubusercontent.com/thesugarat/portainer_templates-1/master/Images/letsencrypt.png",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/letsencrypt.png",
|
||||
"platform": "linux",
|
||||
"ports": [
|
||||
"80/tcp",
|
||||
|
@ -1467,7 +1467,7 @@
|
|||
"note": "",
|
||||
"description": "Changedetection.io provides free, open-source web page monitoring, notification and change detection. (https://github.com/dgtlmoon/changedetection.io)",
|
||||
"platform": "linux",
|
||||
"logo": "https://github.com/linuxserver/docker-templates/raw/master/linuxserver.io/img/changedetection-icon.png",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/changedetection.io.png",
|
||||
"image": "linuxserver/changedetection.io:latest",
|
||||
"env": [
|
||||
{
|
||||
|
@ -1841,7 +1841,7 @@
|
|||
"note": "",
|
||||
"description": "[Embystat](https://github.com/mregni/EmbyStat) is a personal web server that can calculate all kinds of statistics from your (local) Emby server. Just install this on your server and let him calculate all kinds of fun stuff.",
|
||||
"platform": "linux",
|
||||
"logo": "https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/embystat-logo.png",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/embystat.png",
|
||||
"image": "linuxserver/embystat:latest",
|
||||
"env": [
|
||||
{
|
||||
|
@ -2686,7 +2686,7 @@
|
|||
"note": "",
|
||||
"description": "SWAG - Secure Web Application Gateway (formerly known as letsencrypt, no relation to Let's Encrypt\u2122) sets up an Nginx webserver and reverse proxy with php support and a built-in certbot client that automates free SSL server certificate generation and renewal processes (Let's Encrypt and ZeroSSL). It also contains fail2ban for intrusion prevention.",
|
||||
"platform": "linux",
|
||||
"logo": "https://github.com/linuxserver/docker-templates/raw/master/linuxserver.io/img/swag.gif",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/linuxserverio.png",
|
||||
"image": "linuxserver/swag:latest",
|
||||
"env": [
|
||||
{
|
||||
|
@ -2994,38 +2994,6 @@
|
|||
],
|
||||
"restart_policy": "unless-stopped"
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Authentication"
|
||||
],
|
||||
"note": "recommend requirements: a host with at least 2 CPU cores and 2 GB of RAM",
|
||||
"description": "authentik is an open-source Identity Provider focused on flexibility and versatility. You can use authentik in an existing environment to add support for new protocols, implement sign-up/recovery/etc. in your application so you don't have to deal with it, and many other things.",
|
||||
"env": [
|
||||
{
|
||||
"default": "1000",
|
||||
"label": "PUID",
|
||||
"name": "PUID"
|
||||
},
|
||||
{
|
||||
"default": "1000",
|
||||
"label": "PGID",
|
||||
"name": "PGID"
|
||||
},
|
||||
{
|
||||
"label": "PORT",
|
||||
"name": "PORT"
|
||||
}
|
||||
],
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/authentik.png",
|
||||
"name": "authentik",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/authentik.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Authentik",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Productivity"
|
||||
|
@ -3043,7 +3011,7 @@
|
|||
}
|
||||
],
|
||||
"image": "zadam/trilium:latest",
|
||||
"logo": "https://www.saashub.com/images/app/service_logos/55/2901389fab77/large.png?1561117248",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/trilium.png",
|
||||
"name": "trilium",
|
||||
"platform": "linux",
|
||||
"ports": [
|
||||
|
@ -3059,50 +3027,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Communication"
|
||||
],
|
||||
"description": "Rocket.Chat is an open-source fully customizable communications platform developed in JavaScript for organizations with high standards of data protection.",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/rocketchat.png",
|
||||
"note": "Rocket.Chat Server Container",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/rocketchat.yml",
|
||||
"url": "https://github.com/mycroftwilde/portainer_templates"
|
||||
},
|
||||
"title": "Rocket Chat",
|
||||
"name": "rocketchat",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"description": "Joplin is an open-source note-taking app",
|
||||
"env": [
|
||||
{
|
||||
"default": "22300",
|
||||
"label": "PORT",
|
||||
"name": "PORT"
|
||||
},
|
||||
{
|
||||
"default": "http://joplin.yourdomain.tld:22300",
|
||||
"label": "URL",
|
||||
"name": "URL"
|
||||
}
|
||||
],
|
||||
"logo": "https://raw.githubusercontent.com/laurent22/joplin/master/Assets/SquareIcon512.png",
|
||||
"note": "Joplin is an open-source note-taking app",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/joplin.yml",
|
||||
"url": "https://github.com/mycroftwilde/portainer_templates"
|
||||
},
|
||||
"title": "Joplin",
|
||||
"name": "joplin",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Dashboard"
|
||||
|
@ -3435,7 +3359,7 @@
|
|||
"type": 1,
|
||||
"volumes": [
|
||||
{
|
||||
"bind": "/portainer/Downloads",
|
||||
"bind": "/home/docker/downloads",
|
||||
"container": "/data"
|
||||
},
|
||||
{
|
||||
|
@ -3552,7 +3476,7 @@
|
|||
],
|
||||
"description": "YoutubeDL-Material is a Material Design frontend for youtube-dl. It's coded using Angular 9 for the frontend, and Node.js on the backend.",
|
||||
"image": "tzahi12345/youtubedl-material:latest",
|
||||
"logo": "https://raw.githubusercontent.com/Qballjos/portainer_templates/master/Images/ytdlm.png",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/youtubedl.png",
|
||||
"name": "youtubedl-material",
|
||||
"platform": "linux",
|
||||
"ports": [
|
||||
|
@ -4375,7 +4299,7 @@
|
|||
"container": "/config"
|
||||
},
|
||||
{
|
||||
"bind": "/portainer/Downloads",
|
||||
"bind": "/home/docker/Downloads",
|
||||
"container": "/app/qBittorrent/downloads"
|
||||
}
|
||||
]
|
||||
|
@ -4677,50 +4601,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Finance",
|
||||
"Productivity"
|
||||
],
|
||||
"description": "Invoices, Expenses and Tasks built with Laravel and Flutter.",
|
||||
"env": [
|
||||
{
|
||||
"default": "invoice.my.domain",
|
||||
"label": "URL",
|
||||
"name": "URL"
|
||||
},
|
||||
{
|
||||
"label": "APP_KEY",
|
||||
"name": "APP_KEY"
|
||||
},
|
||||
{
|
||||
"label": "TZ",
|
||||
"name": "TZ"
|
||||
},
|
||||
{
|
||||
"label": "DATABASE_PASSWORD",
|
||||
"name": "DATABASE_PASSWORD"
|
||||
},
|
||||
{
|
||||
"label": "MYSQL_ROOT_PASSWORD",
|
||||
"name": "MYSQL_ROOT_PASSWORD"
|
||||
},
|
||||
{
|
||||
"label": "PORT",
|
||||
"name": "PORT"
|
||||
}
|
||||
],
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/invoiceninja.png",
|
||||
"name": "invoice_ninja",
|
||||
"note": "The database user is invoice_ninja and the database is ninja_db. Please generate an app key following the documentation <a href='https://github.com/invoiceninja/dockerfiles'>here</a>. ",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/invoice-ninja.yml",
|
||||
"url": "https://github.com/SelfhostedPro/selfhosted_templates"
|
||||
},
|
||||
"title": "Invoice Ninja",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Gaming"
|
||||
|
@ -4827,7 +4707,7 @@
|
|||
"container": "/etc/localtime"
|
||||
},
|
||||
{
|
||||
"bind": "/portainer/Downloads",
|
||||
"bind": "/home/docker/Downloads",
|
||||
"container": "/downloads"
|
||||
},
|
||||
{
|
||||
|
@ -4854,7 +4734,7 @@
|
|||
"container": "/app/configs"
|
||||
},
|
||||
{
|
||||
"bind": "/portainer/Downloads",
|
||||
"bind": "/home/docker/Downloads",
|
||||
"container": "/downloads"
|
||||
}
|
||||
],
|
||||
|
@ -4913,68 +4793,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Downloaders"
|
||||
],
|
||||
"description": "A docker image with qBittorrent and the Flood UI, also optional WireGuard VPN support.",
|
||||
"logo": "https://hotio.dev/img/image-logos/flood.svg",
|
||||
"name": "flood",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/flood.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Flood",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Multimedia",
|
||||
"Productivity"
|
||||
],
|
||||
"description": "PhotoPrism is an AI-powered app for browsing, organizing & sharing your photo collection. It makes use of the latest technologies to tag and find pictures automatically without getting in your way.| Copy as Custom stack and EDIT environment variables.",
|
||||
"logo": "https://photoprism.app/static/img/logo.svg",
|
||||
"name": "photoprism",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/photoprism.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Photoprism",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Multimedia",
|
||||
"AI"
|
||||
],
|
||||
"description": "Immich is a high performance self-hosted photo and video backup solution.",
|
||||
"logo": "https://github.com/immich-app/immich/raw/main/design/immich-logo.svg",
|
||||
"name": "immich",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/immich.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Immich",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Tools"
|
||||
],
|
||||
"description": "The recipe manager that allows you to manage your ever growing collection of digital recipes.",
|
||||
"logo": "https://docs.tandoor.dev/logo_color.svg",
|
||||
"name": "tandoor",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/tandoor.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Tandoor",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
"name": "influxdb",
|
||||
|
@ -5023,36 +4841,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Media Server"
|
||||
],
|
||||
"description": "Your media enjoyed through a minimal lightweight media server.",
|
||||
"logo": "https://github.com/midarrlabs/midarr-server/raw/master/priv/static/logo.svg",
|
||||
"name": "midarr",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/midarr.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Midarr",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Development"
|
||||
],
|
||||
"description": "Appwrite is a self-hosted backend-as-a-service platform that provides developers with all the core APIs required to build any application.",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/appwrite.png",
|
||||
"name": "appwrite",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/appwrite.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Appwrite",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Media Server",
|
||||
|
@ -5121,72 +4909,6 @@
|
|||
],
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Downloaders",
|
||||
"Multimedia"
|
||||
],
|
||||
"description": "Autobrr is the modern download automation tool for torrents.",
|
||||
"logo": "https://autobrr.com/img/logo.png",
|
||||
"name": "autobrr",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/autobrr.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Autobrr",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Monitoring"
|
||||
],
|
||||
"description": "Glances is an open-source system cross-platform monitoring tool. It allows real-time monitoring of various aspects of your system such as CPU, memory, disk, network usage etc.",
|
||||
"logo": "https://raw.githubusercontent.com/nicolargo/glances/develop/docs/_static/glances-responsive-webdesign.png",
|
||||
"name": "glances",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/glances.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Glances",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Development",
|
||||
"Low Code",
|
||||
"No code"
|
||||
],
|
||||
"description": "Tooljet is an Open-source low-code application development platform for building and deploying business applications.",
|
||||
"logo": "https://uploads-ssl.webflow.com/6266634263b9179f76b2236e/63aaa161e3b3be42ec50eb6f_Logomark.svg",
|
||||
"name": "tooljet",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/tooljet.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Tooljet",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Productivity",
|
||||
"Development",
|
||||
"Low-code",
|
||||
"No-code"
|
||||
],
|
||||
"description": "Budibase allows no-code users to build apps quickly, with more functionality available with a little bit of inline code.",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/budibase.png",
|
||||
"name": "budibase",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/budibase.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Budibase",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Development"
|
||||
|
@ -5209,68 +4931,6 @@
|
|||
],
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Networking",
|
||||
"Monitoring"
|
||||
],
|
||||
"description": "Lazytainer monitors network traffic to containers. If there is traffic, the container runs, otherwise the container is stopped/paused.",
|
||||
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/lazytainer.png",
|
||||
"name": "lazytainer",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/lazytainer.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Lazytainer",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Finance"
|
||||
],
|
||||
"description": "I hate money is a web application made to ease shared budget management. It keeps track of who bought what, when, and for whom; and helps to settle the bills.",
|
||||
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/ihatemoney.png",
|
||||
"name": "ihatemoney",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/ihatemoney.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "I hate money",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Media Server",
|
||||
"LDAP"
|
||||
],
|
||||
"description": "Fireshare: Share your game clips, videos, or other media via unique links.",
|
||||
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/fireshare.png",
|
||||
"name": "fireshare",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/fireshare.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Fireshare",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Productivity"
|
||||
],
|
||||
"description": "Xwiki s a free wiki software platform written in Java with a design emphasis on extensibility. XWiki is an enterprise wiki.",
|
||||
"logo": "https://upload.wikimedia.org/wikipedia/commons/e/e2/Logo-xwikiorange.svg",
|
||||
"name": "xwiki",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/xwiki.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Xwiki",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Productivity"
|
||||
|
@ -5339,21 +4999,6 @@
|
|||
"5055:5055/tcp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Productivity"
|
||||
],
|
||||
"description": "Trudesk is an Open Source Help Desk Software and Ticketing System",
|
||||
"logo": "https://trudesk.io/wp-content/uploads/2019/10/logo-med.png",
|
||||
"name": "trudesk",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/trudesk.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Trudesk",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Dashboard"
|
||||
|
@ -5376,23 +5021,6 @@
|
|||
],
|
||||
"privileged": true
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Productivity",
|
||||
"Development",
|
||||
"No-code"
|
||||
],
|
||||
"description": "Open source no-code database and Airtable alternative.Create your own online database without technical experience. Our user friendly no-code tool gives you the powers of a developer without leaving your browser.",
|
||||
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/baserow.png",
|
||||
"name": "baserow",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/baserow.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"title": "Baserow",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"name": "nocodb",
|
||||
"title": "NocoDB",
|
||||
|
@ -5454,25 +5082,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Analytics",
|
||||
"Tools"
|
||||
],
|
||||
"description": "Google Analytics alternative that protects your data and your customers' privacy",
|
||||
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/matomo.png",
|
||||
"name": "matomo",
|
||||
"platform": "linux",
|
||||
"repository": {
|
||||
"stackfile": "Template/Stack/matomo.yml",
|
||||
"url": "https://github.com/xneo1/portainer_templates"
|
||||
},
|
||||
"ports": [
|
||||
"8282:80/tcp"
|
||||
],
|
||||
"title": "Matomo",
|
||||
"type": 3
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
"Tools"
|
||||
|
@ -5516,11 +5125,11 @@
|
|||
"type": 1,
|
||||
"volumes": [
|
||||
{
|
||||
"bind": "/portainer/Files/AppData/Adguard/Workdir",
|
||||
"bind": "/home/docker/Files/AppData/Adguard/Workdir",
|
||||
"container": "/opt/adguardhome/work"
|
||||
},
|
||||
{
|
||||
"bind": "/portainer/Files/AppData/Adguard/Conf",
|
||||
"bind": "/home/docker/Files/AppData/Adguard/Conf",
|
||||
"container": "/opt/adguardhome/conf"
|
||||
}
|
||||
]
|
||||
|
@ -5611,7 +5220,7 @@
|
|||
"note": "",
|
||||
"description": "Elasticsearch is a distributed, RESTful search and analytics engine capable of solving a growing number of use cases. It provides a distributed, multitenant-capable full-text search engine with an HTTP web interface and schema-free JSON documents. <a href='https://www.elastic.co/' target='_blank'>Website</a>. <a href='https://hub.docker.com/_/elasticsearch' target='_blank'>Docker Hub</a>",
|
||||
"logo": "https://raw.githubusercontent.com/walkxcode/dashboard-icons/main/png/elasticsearch.png",
|
||||
"image": "elasticsearch",
|
||||
"image": "elasticsearch:8.13.4",
|
||||
"categories": [
|
||||
"Database"
|
||||
],
|
||||
|
@ -5914,7 +5523,7 @@
|
|||
"note": "",
|
||||
"description": "Nvidia HWA Test is a test container for NVIDIA hardware acceleration. Start the container then check the logs to confirm your output matches the example from this page: <a href='https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/sample-workload.html'>Running a Sample Workload</a>. <a href='https://www.nvidia.com/' target='_blank'>Website</a>. <a href='https://hub.docker.com/r/nvidia/cuda' target='_blank'>Docker Hub</a>",
|
||||
"platform": "linux",
|
||||
"logo": "https://avatars.githubusercontent.com/u/1728152",
|
||||
"logo": "https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/nvidia.png",
|
||||
"image": "nvidia/cuda:12.2.0-base-ubuntu20.04",
|
||||
"env": [
|
||||
{
|
2
templates/tmp/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
250
utils/install.js
Normal file
|
@ -0,0 +1,250 @@
|
|||
import { writeFileSync, mkdirSync, readFileSync, readdirSync, writeFile } from "fs";
|
||||
import yaml from 'js-yaml';
|
||||
import { execSync } from "child_process";
|
||||
import { docker } from "../server.js";
|
||||
import DockerodeCompose from "dockerode-compose";
|
||||
import { Syslog } from "../database/models.js";
|
||||
import { addAlert } from "../controllers/dashboard.js";
|
||||
|
||||
// This entire page hurts to look at.
|
||||
export const Install = async (req, res) => {
|
||||
|
||||
let data = req.body;
|
||||
let name = data.name;
|
||||
|
||||
let containers = await docker.listContainers({ all: true });
|
||||
for (let i = 0; i < containers.length; i++) {
|
||||
if (containers[i].Names[0].includes(name)) {
|
||||
addAlert(req.session, 'danger', `App ${name} already exists. Please remove it first.`);
|
||||
res.redirect('/');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.compose) {
|
||||
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
writeFileSync(`./templates/compose/${name}/compose.yaml`, req.body.compose, function (err) { console.log(err) });
|
||||
let compose = new DockerodeCompose(docker, `./templates/compose/${name}/compose.yaml`, `${name}`);
|
||||
addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`);
|
||||
try {
|
||||
(async () => {
|
||||
await compose.pull();
|
||||
await compose.up();
|
||||
|
||||
await Syslog.create({
|
||||
user: req.session.user,
|
||||
email: null,
|
||||
event: "App Installation",
|
||||
message: `${app} installed successfully`,
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
})();
|
||||
} catch (err) {
|
||||
await Syslog.create({
|
||||
user: req.session.user,
|
||||
email: null,
|
||||
event: "App Installation",
|
||||
message: `${app} installation failed: ${err}`,
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
let { service_name, image, command_check, command, net_mode, restart_policy } = data;
|
||||
let { port0, port1, port2, port3, port4, port5 } = data;
|
||||
let { volume0, volume1, volume2, volume3, volume4, volume5 } = data;
|
||||
let { env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11 } = data;
|
||||
let { label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11 } = data;
|
||||
|
||||
let ports = [port0, port1, port2, port3, port4, port5]
|
||||
|
||||
let docker_volumes = [];
|
||||
|
||||
addAlert(req.session, 'success', `Installing ${name}. It should appear on the dashboard shortly.`);
|
||||
|
||||
if (image.startsWith('https://')){
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
execSync(`curl -o ./appdata/${name}/${name}_stack.yml -L ${image}`);
|
||||
console.log(`Downloaded stackfile: ${image}`);
|
||||
let stackfile = yaml.load(readFileSync(`./appdata/${name}/${name}_stack.yml`, 'utf8'));
|
||||
let services = Object.keys(stackfile.services);
|
||||
|
||||
for ( let i = 0; i < services.length; i++ ) {
|
||||
try {
|
||||
console.log(stackfile.services[Object.keys(stackfile.services)[i]].environment);
|
||||
} catch { console.log('no env') }
|
||||
}
|
||||
} else {
|
||||
|
||||
let compose_file = `version: '3'`;
|
||||
compose_file += `\nservices:`
|
||||
compose_file += `\n ${service_name}:`
|
||||
compose_file += `\n container_name: ${name}`;
|
||||
compose_file += `\n image: ${image}`;
|
||||
|
||||
// Command
|
||||
if (command_check == 'on') {
|
||||
compose_file += `\n command: ${command}`
|
||||
}
|
||||
|
||||
// Network mode
|
||||
if (net_mode == 'host') {
|
||||
compose_file += `\n network_mode: 'host'`
|
||||
}
|
||||
else if (net_mode != 'host' && net_mode != 'docker') {
|
||||
compose_file += `\n network_mode: '${net_mode}'`
|
||||
}
|
||||
|
||||
// Restart policy
|
||||
if (restart_policy != '') {
|
||||
compose_file += `\n restart: ${restart_policy}`
|
||||
}
|
||||
|
||||
// Ports
|
||||
for (let i = 0; i < ports.length; i++) {
|
||||
if ((ports[i] == 'on') && (net_mode != 'host')) {
|
||||
compose_file += `\n ports:`
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < ports.length; i++) {
|
||||
if ((ports[i] == 'on') && (net_mode != 'host')) {
|
||||
compose_file += `\n - ${data[`port_${i}_external`]}:${data[`port_${i}_internal`]}/${data[`port_${i}_protocol`]}`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Volumes
|
||||
let volumes = [volume0, volume1, volume2, volume3, volume4, volume5]
|
||||
|
||||
for (let i = 0; i < volumes.length; i++) {
|
||||
if (volumes[i] == 'on') {
|
||||
compose_file += `\n volumes:`
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < volumes.length; i++) {
|
||||
|
||||
// if volume is on and neither bind or container is empty, it's a bind mount (ex /mnt/user/appdata/config:/config )
|
||||
if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] != '') && (data[`volume_${i}_container`] != '')) {
|
||||
compose_file += `\n - ${data[`volume_${i}_bind`]}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
}
|
||||
|
||||
// if bind is empty create a docker volume (ex container_name_config:/config) convert any '/' in container name to '_'
|
||||
else if ((data[`volume${i}`] == 'on') && (data[`volume_${i}_bind`] == '') && (data[`volume_${i}_container`] != '')) {
|
||||
let volume_name = data[`volume_${i}_container`].replace(/\//g, '_');
|
||||
compose_file += `\n - ${name}_${volume_name}:${data[`volume_${i}_container`]}:${data[`volume_${i}_readwrite`]}`
|
||||
docker_volumes.push(`${name}_${volume_name}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Environment variables
|
||||
let env_vars = [env0, env1, env2, env3, env4, env5, env6, env7, env8, env9, env10, env11]
|
||||
|
||||
for (let i = 0; i < env_vars.length; i++) {
|
||||
if (env_vars[i] == 'on') {
|
||||
compose_file += `\n environment:`
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < env_vars.length; i++) {
|
||||
if (env_vars[i] == 'on') {
|
||||
compose_file += `\n - ${data[`env_${i}_name`]}=${data[`env_${i}_default`]}`
|
||||
}
|
||||
}
|
||||
|
||||
// Labels
|
||||
let labels = [label0, label1, label2, label3, label4, label5, label6, label7, label8, label9, label10, label11]
|
||||
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
if (labels[i] == 'on') {
|
||||
compose_file += `\n labels:`
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
if (data[`label${i}`] == 'on') {
|
||||
compose_file += `\n - ${data[`label_${i}_name`]}=${data[`label_${i}_value`]}`
|
||||
}
|
||||
}
|
||||
|
||||
// Privileged mode
|
||||
if (data.privileged == 'on') {
|
||||
compose_file += `\n privileged: true`
|
||||
}
|
||||
|
||||
// Hardware acceleration
|
||||
for (let i = 0; i < env_vars.length; i++) {
|
||||
if ((env_vars[i] == 'on') && (data[`env_${i}_name`] == 'DRINODE')) {
|
||||
compose_file += `\n deploy:`
|
||||
compose_file += `\n resources:`
|
||||
compose_file += `\n reservations:`
|
||||
compose_file += `\n devices:`
|
||||
compose_file += `\n - driver: nvidia`
|
||||
compose_file += `\n count: 1`
|
||||
compose_file += `\n capabilities: [gpu]`
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// add any docker volumes to the docker-compose file
|
||||
if ( docker_volumes.length > 0 ) {
|
||||
compose_file += `\n`
|
||||
compose_file += `\nvolumes:`
|
||||
|
||||
// check docker_volumes for duplicates and remove them completely
|
||||
docker_volumes = docker_volumes.filter((item, index) => docker_volumes.indexOf(item) === index)
|
||||
|
||||
for (let i = 0; i < docker_volumes.length; i++) {
|
||||
if ( docker_volumes[i] != '') {
|
||||
compose_file += `\n ${docker_volumes[i]}:`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
mkdirSync(`./appdata/${name}`, { recursive: true });
|
||||
writeFileSync(`./appdata/${name}/docker-compose.yml`, compose_file, function (err) { console.log(err) });
|
||||
var compose = new DockerodeCompose(docker, `./appdata/${name}/docker-compose.yml`, `${name}`);
|
||||
} catch {
|
||||
await Syslog.create({
|
||||
user: req.session.user,
|
||||
email: null,
|
||||
event: "App Installation",
|
||||
message: `${name} installation failed - error creating directory or compose file`,
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
(async () => {
|
||||
await compose.pull();
|
||||
await compose.up();
|
||||
|
||||
await Syslog.create({
|
||||
user: req.session.user,
|
||||
email: null,
|
||||
event: "App Installation",
|
||||
message: `${name} installed successfully`,
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
})();
|
||||
} catch (err) {
|
||||
await Syslog.create({
|
||||
user: req.session.user,
|
||||
email: null,
|
||||
event: "App Installation",
|
||||
message: `${name} installation failed: ${err}`,
|
||||
ip: req.socket.remoteAddress
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
res.redirect('/');
|
||||
}
|
|
@ -6,17 +6,22 @@ export const Uninstall = async (req, res) => {
|
|||
|
||||
let { confirm, service_name } = req.body;
|
||||
|
||||
console.log(`Uninstalling ${service_name}...`);
|
||||
|
||||
if (confirm == 'Yes') {
|
||||
var containerName = docker.getContainer(`${service_name}`);
|
||||
|
||||
let containerName = docker.getContainer(service_name);
|
||||
console.log(`Stopping ${service_name}...`)
|
||||
try {
|
||||
await containerName.stop();
|
||||
} catch {
|
||||
console.log(`Error stopping ${service_name} container`);
|
||||
}
|
||||
try {
|
||||
await containerName.remove();
|
||||
|
||||
|
||||
try {
|
||||
console.log(`Removing ${service_name}...`);
|
||||
containerName.remove();
|
||||
|
||||
const syslog = await Syslog.create({
|
||||
user: req.session.user,
|
||||
email: null,
|
||||
|
@ -35,7 +40,10 @@ export const Uninstall = async (req, res) => {
|
|||
ip: req.socket.remoteAddress
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(`Didn't confirm uninstallation of ${service_name}...`);
|
||||
}
|
||||
|
||||
res.redirect('/');
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Account</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Account Settings
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="card">
|
||||
<div class="row g-0">
|
||||
<div class="col-3 d-none d-md-block border-end">
|
||||
<div class="card-body">
|
||||
<h4 class="subheader">Business settings</h4>
|
||||
<div class="list-group list-group-transparent">
|
||||
<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center active">Accounts</a>
|
||||
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">My Notifications</a>
|
||||
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Connected Apps</a> -->
|
||||
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center">Settings</a>
|
||||
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Billing & Invoices</a> -->
|
||||
</div>
|
||||
<h4 class="subheader mt-4">Experience</h4>
|
||||
<div class="list-group list-group-transparent">
|
||||
<a href="#" class="list-group-item list-group-item-action">Credits</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col d-flex flex-column">
|
||||
<div class="card-body">
|
||||
<h2 class="mb-4">My Account</h2>
|
||||
<h3 class="card-title">Profile Details</h3>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="avatar avatar-xl"><%- avatar %></span>
|
||||
</div>
|
||||
<div class="col-auto"><a href="#" class="btn">
|
||||
Change avatar
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-auto"><a href="#" class="btn btn-ghost-danger">
|
||||
Delete avatar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Profile</h3>
|
||||
<div class="row g-3">
|
||||
<div class="col-md">
|
||||
<div class="form-label">Full Name</div>
|
||||
<input type="text" class="form-control" value="<%= name %>" readonly="<%= name %>">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">First Name</div>
|
||||
<input type="text" class="form-control" value="<%= first_name %>" readonly="<%= first_name %>">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Last Name</div>
|
||||
<input type="text" class="form-control" value="<%= last_name %>" readonly="<%= last_name %>">
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Email</h3>
|
||||
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
|
||||
<div>
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<input type="text" class="form-control w-auto" value="<%= email %>" readonly="<%= email %>">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="btn">Change</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Password</h3>
|
||||
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
|
||||
<div>
|
||||
<a href="#" class="btn">
|
||||
Set new password
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Public profile</h3>
|
||||
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
|
||||
you.</p>
|
||||
<div>
|
||||
<label class="form-check form-switch form-switch-lg">
|
||||
<input class="form-check-input" type="checkbox" >
|
||||
<span class="form-check-label form-check-label-on">You're currently visible</span>
|
||||
<span class="form-check-label form-check-label-off">You're
|
||||
currently invisible</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent mt-auto">
|
||||
<div class="btn-list justify-content-end">
|
||||
<a href="#" class="btn">
|
||||
Cancel
|
||||
</a>
|
||||
<a href="#" class="btn btn-primary">
|
||||
Submit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('footer.ejs') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
</body>
|
||||
</html>
|
131
views/account.html
Normal file
|
@ -0,0 +1,131 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Account</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('partials/navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Settings
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="card">
|
||||
<div class="row g-0">
|
||||
|
||||
<%- include('partials/sidebar.html') %>
|
||||
|
||||
<div class="col d-flex flex-column">
|
||||
<div class="card-body">
|
||||
<h2 class="mb-4">My Account</h2>
|
||||
<h3 class="card-title">Profile Details</h3>
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto"><span class="avatar avatar-xl"><%- avatar %></span>
|
||||
</div>
|
||||
<div class="col-auto"><a href="#" class="btn">
|
||||
Change avatar
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-auto"><a href="#" class="btn btn-ghost-danger">
|
||||
Delete avatar
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Profile</h3>
|
||||
<div class="row g-3">
|
||||
<div class="col-md">
|
||||
<div class="form-label">Full Name</div>
|
||||
<input type="text" class="form-control" value="<%= name %>" readonly="<%= name %>">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">First Name</div>
|
||||
<input type="text" class="form-control" value="<%= first_name %>" readonly="<%= first_name %>">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Last Name</div>
|
||||
<input type="text" class="form-control" value="<%= last_name %>" readonly="<%= last_name %>">
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Email</h3>
|
||||
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
|
||||
<div>
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<input type="text" class="form-control w-auto" value="<%= email %>" readonly="<%= email %>">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="btn">Change</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Password</h3>
|
||||
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
|
||||
<div>
|
||||
<a href="#" class="btn">
|
||||
Set new password
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Public profile</h3>
|
||||
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
|
||||
you.</p>
|
||||
<div>
|
||||
<label class="form-check form-switch form-switch-lg">
|
||||
<input class="form-check-input" type="checkbox" >
|
||||
<span class="form-check-label form-check-label-on">You're currently visible</span>
|
||||
<span class="form-check-label form-check-label-off">You're
|
||||
currently invisible</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent mt-auto">
|
||||
<div class="btn-list justify-content-end">
|
||||
<a href="#" class="btn">
|
||||
Cancel
|
||||
</a>
|
||||
<a href="#" class="btn btn-primary">
|
||||
Submit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
</body>
|
||||
</html>
|
149
views/apps.ejs
|
@ -1,149 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Apps</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Apps
|
||||
</h2>
|
||||
<div class="text-secondary mt-1"><%= list_start %> - <%= list_end %> of <%= app_count %> Apps.</div>
|
||||
</div>
|
||||
<!-- Page title actions -->
|
||||
<div class="col-auto ms-auto d-print-none">
|
||||
|
||||
<div class="d-flex">
|
||||
<form action="/apps" id="search" name="search" method="POST">
|
||||
<input type="search" class="form-control" name="search" placeholder="Search apps…">
|
||||
</form>
|
||||
<input type="submit" form="search" class="btn btn-outline-success h-50" value="search">
|
||||
|
||||
<div class="card-actions btn-actions">
|
||||
<div class="card-actions btn-actions">
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Change Templates">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-settings" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" /><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" /></svg>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<a class="dropdown-item" href="#">Default Template</a>
|
||||
<a class="dropdown-item" href="#">Compose Files</a>
|
||||
<a class="dropdown-item" href="#">Custom Template</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-cards">
|
||||
|
||||
<%- apps_list %>
|
||||
|
||||
</div>
|
||||
<div class="d-flex mt-4">
|
||||
<ul class="pagination ms-auto">
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="<%- prev %>" tabindex="-1" aria-disabled="true">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-left -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 6l-6 6l6 6" /></svg>
|
||||
prev
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item"><a class="page-link" href="/apps?page=1">1</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps?page=2">2</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps?page=3">3</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps?page=4">4</a></li>
|
||||
<li class="page-item"><a class="page-link" href="/apps?page=5">5</a></li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="<%- next %>">
|
||||
next <!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
<!-- <script>
|
||||
async function sendData(form) {
|
||||
const formData = new FormData(form);
|
||||
const jsonData = {};
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
jsonData[key] = value;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/install", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(jsonData),
|
||||
});
|
||||
console.log(await response.json());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const buttonId = event.target.id;
|
||||
console.log(buttonId);
|
||||
const form = document.querySelector(`#${buttonId}`);
|
||||
|
||||
if (form) {
|
||||
sendData(form);
|
||||
} else {
|
||||
console.error(`Form not found for button with ID: ${buttonId}`);
|
||||
}
|
||||
});
|
||||
</script> -->
|
||||
|
||||
</body>
|
||||
</html>
|
173
views/apps.html
Normal file
|
@ -0,0 +1,173 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Apps</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
|
||||
<%- include('partials/navbar.html') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="mt-3">
|
||||
<div class="container-xl">
|
||||
<div class="row row-cards">
|
||||
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-auto btn"><%= list_start %> - <%= list_end %> of <%= app_count %> Apps</div>
|
||||
<%- remove_button %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="btn me-2">
|
||||
Category:
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Everything</button>
|
||||
<form action="/apps" method="POST">
|
||||
<ul class="dropdown-menu">
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Other"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Productivity"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Tools"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Dashboard"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Communication"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Media"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="CMS"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Monitoring"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="LDAP"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Arr"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Paid"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Database"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Gaming"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Finance"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Networking"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Authentication"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Development"></li>
|
||||
<li><input type="submit" class="dropdown-item" name="search" value="Downloader"></li>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<dropdown class="me-2">
|
||||
<button class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Default Templates</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="/apps">Default Templates</a></li>
|
||||
<li><a class="dropdown-item" href="/apps/1/compose">Compose Files</a></li>
|
||||
<%- json_templates %>
|
||||
</ul>
|
||||
</dropdown>
|
||||
<button class="btn" name="Import" id="Import" data-hx-get="/import_modal" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Import</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<div class="text-secondary d-flex align-items-center">
|
||||
<form action="/apps" id="search" name="search" method="POST" class="d-flex">
|
||||
<input type="search" class="form-control me-2" name="search" placeholder="Search apps…" >
|
||||
<input type="submit" form="search" class="btn" value="Search">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page body -->
|
||||
<div class="mt-3">
|
||||
<div class="container-xl">
|
||||
<div class="row row-cards">
|
||||
|
||||
<%- apps_list %>
|
||||
|
||||
<!-- HTMX Target-->
|
||||
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Loading</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<div class="spinner-border"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="d-flex mt-4">
|
||||
<ul class="pagination ms-auto">
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="<%- prev %>" tabindex="-1" aria-disabled="true">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/chevron-left -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 6l-6 6l6 6" /></svg>
|
||||
prev
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<%- pages %>
|
||||
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="<%- next %>">
|
||||
next <!-- Download SVG icon from http://tabler-icons.io/i/chevron-right -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 6l6 6l-6 6" /></svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
</body>
|
||||
</html>
|
1011
views/dashboard.ejs
280
views/dashboard.html
Normal file
|
@ -0,0 +1,280 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Dashboard</title>
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/meters.css" rel="stylesheet"/>
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/htmx-sse.js"></script>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
|
||||
<div class="page">
|
||||
|
||||
<%- include('partials/navbar.html') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards" hx-ext="sse" sse-connect="/sse">
|
||||
|
||||
<div class="col-12">
|
||||
<div class="row row-cards">
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-green text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- HTMX -->
|
||||
<div class="col" name="CPU" id="green" data-hx-get="/stats" data-hx-trigger="load, every 1s">
|
||||
<div class="font-weight-medium">
|
||||
<label class="cpu-text mb-1" for="cpu">CPU 0%</label>
|
||||
</div>
|
||||
<div class="cpu-bar meter animate green">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-blue text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- HTMX -->
|
||||
<div class="col" name="RAM" id="blue" data-hx-get="/stats" data-hx-trigger="load, every 2s">
|
||||
<div class="font-weight-medium">
|
||||
<label class="ram-text mb-1" for="ram">RAM 0%</label>
|
||||
</div>
|
||||
<div class="ram-bar meter animate blue">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-purple text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- HTMX -->
|
||||
<div class="col" name="NET" id="purple" data-hx-get="/stats" data-hx-trigger="load, every 2s">
|
||||
<div class="font-weight-medium">
|
||||
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
|
||||
</div>
|
||||
<div class="ram-bar meter animate purple">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-orange text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- HTMX -->
|
||||
<div class="col" name="DISK" id="orange" data-hx-get="/stats" data-hx-trigger="load, every 3s">
|
||||
<div class="font-weight-medium">
|
||||
<label class="disk-text mb-1" for="disk">DISK 0%</label>
|
||||
</div>
|
||||
<div class="meter animate orange">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="row row-cards" id="containers">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTMX -->
|
||||
<div class="col-12">
|
||||
<div class="row row-cards" data-hx-post="/dashboard/updates" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTMX Modal Target -->
|
||||
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Loading</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<div class="spinner-border"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal modal-blur fade" id="log_view" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Logs</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body">
|
||||
<h4>Logs:</h4>
|
||||
<div id="logView">
|
||||
<pre>No logs available</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-info" onclick="viewLogs(this)" name="refresh">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/libs/apexcharts/dist/apexcharts.min.js"></script>
|
||||
<script src="/js/tabler.min.js"></script>
|
||||
<script>
|
||||
var options = {
|
||||
chart: {
|
||||
type: "line",
|
||||
height: 40.0,
|
||||
sparkline: {
|
||||
enabled: true
|
||||
},
|
||||
animations: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
},
|
||||
stroke: {
|
||||
width: [3, 1],
|
||||
dashArray: [0, 3],
|
||||
lineCap: "round",
|
||||
curve: "smooth"
|
||||
},
|
||||
series: [{
|
||||
name: "CPU",
|
||||
data: []
|
||||
}, {
|
||||
name: "RAM",
|
||||
data: []
|
||||
}],
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 4
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
padding: 0
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
labels: {
|
||||
padding: 4
|
||||
}
|
||||
},
|
||||
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- SelectAll for the permissions modal -->
|
||||
<script>
|
||||
function selectAll(group) {
|
||||
|
||||
let checkboxes = document.getElementsByName(group);
|
||||
if (checkboxes[0].checked == true) {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = true;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
188
views/images.ejs
|
@ -1,188 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Images</title>
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
<div class="col-12 mt-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Docker Images</h3>
|
||||
<div class="card-options btn-list">
|
||||
<a href="#" class="btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
|
||||
Refresh
|
||||
</a>
|
||||
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#not_add-site">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
|
||||
New Image
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal modal-blur fade" id="add-site" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
|
||||
<form action="/addsite" id="addsite" method="POST">
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Type</div>
|
||||
<select class="form-select" name="type">
|
||||
<option value="reverse_proxy">Reverse Proxy</option>
|
||||
<option value="proxy">Proxy</option>
|
||||
<option value="file_server">File Server</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Domain / Subdomain</label>
|
||||
<input type="text" class="form-control" name="domain" placeholder="media.mydomainname.com">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="row g-2">
|
||||
<div class="col-8">
|
||||
<label class="form-label">Hostname / Host IP</label>
|
||||
<input type="text" class="form-control" name="host" placeholder="localhost">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label">Port</label>
|
||||
<input type="text" class="form-control" name="port" placeholder="8000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="divide-y">
|
||||
<div>
|
||||
<label class="row">
|
||||
<span class="col" title="HTTP Strict Transport Security (HSTS) is a simple and widely supported standard to protect visitors by ensuring that their browsers always connect to a website over HTTPS.">HSTS</span>
|
||||
<span class="col-auto">
|
||||
<label class="form-check form-check-single form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="hsts" checked="" disabled="">
|
||||
</label>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Container</div>
|
||||
<select class="form-select" name="container" disabled="">
|
||||
<option value="0" selected></option>
|
||||
<option value="1">Jellyfin</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
|
||||
<input type="submit" form="addsite" class="btn btn-success" value="Add"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="table-default" class="table-responsive">
|
||||
<table class="table">
|
||||
|
||||
<%- image_list %>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
|
||||
<span class="dropdown">
|
||||
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item" type="submit" formaction="/enablesite">
|
||||
Enable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/disablesite">
|
||||
Disable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/removesite">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
</form>
|
||||
|
||||
<p class="m-0 text-muted ms-auto"><%- image_count %> Images</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Libs JS -->
|
||||
<script src="/libs/list.js/dist/list.min.js" defer></script>
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const list = new List('table-default', {
|
||||
sortClass: 'table-sort',
|
||||
listClass: 'table-tbody',
|
||||
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
|
||||
{ attr: 'data-date', name: 'sort-date' },
|
||||
{ attr: 'data-progress', name: 'sort-progress' },
|
||||
'sort-quantity'
|
||||
]
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function selectAll() {
|
||||
let checkboxes = document.getElementsByName('select');
|
||||
if (checkboxes[0].checked == true) {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = true;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
159
views/images.html
Normal file
|
@ -0,0 +1,159 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Images</title>
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('partials/navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
<div class="col-12 mt-12">
|
||||
<div class="card">
|
||||
<form method="post">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Docker Images</h3>
|
||||
<div class="card-options btn-list">
|
||||
<!-- <a href="#" class="btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
|
||||
Refresh
|
||||
</a> -->
|
||||
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#modals-here">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
|
||||
New Image
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="table-default" class="table-responsive">
|
||||
<table class="table">
|
||||
|
||||
<%- image_list %>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
|
||||
<button class="btn" type="submit" formaction="/images/remove">Remove</button>
|
||||
|
||||
</form>
|
||||
|
||||
<p class="m-0 text-muted ms-auto"><%- image_count %> Images</p>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- HTMX Modal Target -->
|
||||
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">New Image</h5>
|
||||
</div>
|
||||
|
||||
<div class="modal-body text-center">
|
||||
<form method="post" action="/images/add">
|
||||
|
||||
<div class="row row-cards">
|
||||
<div class="col-sm-6 col-md-6">
|
||||
|
||||
</div>
|
||||
<div class="col-sm-6 col-md-6">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-7">
|
||||
<label class="form-label text-muted">Image</label>
|
||||
<input type="text" class="form-control" name="image" placeholder="lllllllillllllillll/dweebui">
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-label text-muted">Tag</label>
|
||||
<input type="text" class="form-control" name="tag" placeholder="latest">
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<button type="submit" class="btn mt-2">Pull</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Libs JS -->
|
||||
<script src="/libs/list.js/dist/list.min.js" defer></script>
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const list = new List('table-default', {
|
||||
sortClass: 'table-sort',
|
||||
listClass: 'table-tbody',
|
||||
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
|
||||
{ attr: 'data-date', name: 'sort-date' },
|
||||
{ attr: 'data-progress', name: 'sort-progress' },
|
||||
'sort-quantity'
|
||||
]
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function selectAll() {
|
||||
let checkboxes = document.getElementsByName('select');
|
||||
if (checkboxes[0].checked == true) {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = true;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -9,7 +9,7 @@
|
|||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('fonts/inter.css');
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
@ -22,19 +22,27 @@
|
|||
<script src="/js/demo-theme.js"></script>
|
||||
<div class="page page-center">
|
||||
<div class="container container-tight py-4">
|
||||
<div class="text-center mb-4">
|
||||
<a href="." class="navbar-brand navbar-brand-autodark"><img src="/static/logo.svg" height="50" alt=""></a>
|
||||
<div class="text-center">
|
||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<img src="/img/logo.png" alt="DweebUI" title="DweebUI" height="100px">
|
||||
</h1>
|
||||
</div>
|
||||
<div class="text-center mb-4">
|
||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<img src="/img/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="card card-md">
|
||||
<div class="card-body">
|
||||
<h2 class="h2 text-center mb-4">Login to your account</h2>
|
||||
<form action="/login" method="POST" novalidate>
|
||||
|
||||
<% if(error) { %>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<%= error %>
|
||||
</div>
|
||||
<% } %>
|
||||
<% if(error) { %>
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<%= error %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Email address</label>
|
||||
|
@ -70,9 +78,12 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
28
views/modals/compose.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Install AppName</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<pre class="text-secondary">AppNote</pre>
|
||||
|
||||
<form action="/install" name="FormId_install" id="FormId_install" method="POST">
|
||||
|
||||
<input type="hidden" name="name" value="AppName"/>
|
||||
<textarea class="form-control" name="compose" rows="30" placeholder="Content..">COMPOSE_CONTENT</textarea>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
|
||||
<input type="submit" form="FormId_install" class="btn btn-success" value="Install"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
956
views/modals/details.html
Normal file
|
@ -0,0 +1,956 @@
|
|||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Details</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<form action="" id="details_modal" method="POST">
|
||||
<div class="row mb-3 align-items-end">
|
||||
<div class="col-lg-5">
|
||||
<label class="form-label">Container Name: </label>
|
||||
<input type="text" class="form-control" name="service_name" value="AppName" hidden/>
|
||||
<input type="text" class="form-control" name="name" value="AppName"/>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">Image: </label>
|
||||
<input type="text" class="form-control" name="image" value="AppImage"/>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label">Restart Policy: </label>
|
||||
<select class="form-select" name="restart_policy" value="">
|
||||
<option value="1">unless-stopped</option>
|
||||
<option value="2">on-failure</option>
|
||||
<option value="3">never</option>
|
||||
<option value="4">always</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label">Network Mode</label>
|
||||
<div class="form-selectgroup-boxes row mb-3">
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="report-type" value="1" class="form-selectgroup-input">
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Host Network</span>
|
||||
<span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="report-type" class="form-selectgroup-input">
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
|
||||
<span class="d-block text-secondary">Containers can communicate using names.</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="report-type" class="form-selectgroup-input">
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
|
||||
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion" id="modal-accordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-1">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1" aria-expanded="false">
|
||||
Ports
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_0_check" type="checkbox" Port0Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">External Port</label>
|
||||
<input type="text" class="form-control" name="port_0_external" value="Port0External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Internal Port</label>
|
||||
<input type="text" class="form-control" name="port_0_internal" value="Port0Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">Protocol</label>
|
||||
<select class="form-select" name="port_0_protocol">
|
||||
<option value="Port0Protocol" selected hidden>Port0Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_1_check" type="checkbox" Port1Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_1_external" value="Port1External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_1_internal" value="Port1Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_1_protocol">
|
||||
<option value="Port1Protocol" selected hidden>Port1Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_2_check" type="checkbox" Port2Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_2_external" value="Port2External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_2_internal" value="Port2Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_2_protocol">
|
||||
<option value="Port2Protocol" selected hidden>Port2Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_3_check" type="checkbox" Port3Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_3_external" value="Port3External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_3_internal" value="Port3Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_3_protocol">
|
||||
<option value="Port3Protocol" selected hidden>Port3Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_4_check" type="checkbox" Port4Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_4_external" value="Port4External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_4_internal" value="Port4Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_4_protocol">
|
||||
<option value="Port4Protocol" selected hidden>Port4Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_5_check" type="checkbox" Port5Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_5_external" value="Port5External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_5_internal" value="Port5Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_5_protocol">
|
||||
<option value="Port5Protocol" selected hidden>Port5Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-2">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-2" aria-expanded="false">
|
||||
Volumes
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<div id="collapse-2" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
|
||||
|
||||
<div class="accordion-body pt-0">
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume_0_check" type="checkbox" Vol0Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_0_bind" value="Vol0Source"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_0_container" value="Vol0Destination"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_0_readwrite">
|
||||
<option value="Vol0RW" selected hidden>Vol0RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume_1_check" type="checkbox" Vol1Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_1_bind" value="Vol1Source"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_1_container" value="Vol1Destination"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_1_readwrite">
|
||||
<option value="Vol1RW" selected hidden>Vol1RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume_2_check" type="checkbox" Vol2Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_2_bind" value="Vol2Source"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_2_container" value="Vol2Destination"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_2_readwrite">
|
||||
<option value="Vol2RW" selected hidden>Vol2RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume_3_check" type="checkbox" Vol3Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_3_bind" value="Vol3Source"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_3_container" value="Vol3Destination"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_3_readwrite">
|
||||
<option value="Vol3RW" selected hidden>Vol3RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume_4_check" type="checkbox" Vol4Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_4_bind" value="Vol4Source"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_4_container" value="Vol4Destination"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_4_readwrite">
|
||||
<option value="Vol4RW" selected hidden>Vol4RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume_5_check" type="checkbox" Vol5Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_5_bind" value="Vol5Source"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_5_container" value="Vol5Destination"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_5_readwrite">
|
||||
<option value="Vol5RW" selected hidden>Vol5RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-3">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-3" aria-expanded="false">
|
||||
Environment Variables
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-3" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_0_check" Env0Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Variable</label>
|
||||
<input type="text" class="form-control" name="env_0_name" value="Env0Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" name="env_0_default" value="Env0Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_1_check" Env1Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_1_name" value="Env1Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_1_default" value="Env1Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_2_check" Env2Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_2_name" value="Env2Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_2_default" value="Env2Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_3_check" Env3Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_3_name" value="Env3Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_3_default" value="Env3Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_4_check" Env4Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_4_name" value="Env4Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_4_default" value="Env4Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_5_check" Env5Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_5_name" value="Env5Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_5_default" value="Env5Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_6_check" Env6Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_6_name" value="Env6Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_6_default" value="Env6Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_7_check" Env7Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_7_name" value="Env7Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_7_default" value="Env7Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_8_check" Env8Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_8_name" value="Env8Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_8_default" value="Env8Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_9_check" Env9Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_9_name" value="Env9Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_9_default" value="Env9Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_10_check" Env10Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_10_name" value="Env10Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_10_default" value="Env10Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_11_check" Env11Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_11_name" value="Env11Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_11_default" value="Env11Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_12_check" Env12Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_12_name" value="Env12Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_12_default" value="Env12Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_12_check" Env13Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_12_name" value="Env13Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_12_default" value="Env13Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_12_check" Env14Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_12_name" value="Env14Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_12_default" value="Env14Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env_12_check" Env15Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_12_name" value="Env15Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_12_default" value="Env15Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-4">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-4" aria-expanded="false">
|
||||
Labels
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-4" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_0_check" Label0Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Variable</label>
|
||||
<input type="text" class="form-control" name="label_0_name" value="Label0Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" name="label_0_value" value="Label0Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_1_check" Label1Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_1_name" value="Label1Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_1_value" value="Label1Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_2_check" Label2Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_2_name" value="Label2Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_2_value" value="Label2Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_3_check" Label3Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_3_name" value="Label3Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_3_value" value="Label3Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_4_check" Label4Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_4_name" value="Label4Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_4_value" value="Label4Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_5_check" Label5Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_5_name" value="Label5Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_5_value" value="Label5Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_6_check" Label6Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_6_name" value="Label6Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_6_value" value="Label6Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_7_check" Label7Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_7_name" value="Label7Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_7_value" value="Label7Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_8_check" Label8Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_8_name" value="Label8Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_8_value" value="Label8Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_9_check" Label9Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_9_name" value="Label9Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_9_value" value="Label9Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_10_check" Label10Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_10_name" value="Label10Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_10_value" value="Label10Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_11_check" Label11Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label11Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label11Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_11_check" Label12Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label12Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label12Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_11_check" Label13Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label13Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label13Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_11_check" Label14Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label14Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label14Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_11_check" Label15Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label15Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label15Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_11_check" Label16Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label16Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label16Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_11_check" Label17Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label17Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label17Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_11_check" Label18Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label18Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label18Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label_11_check" Label19Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label19Key"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label19Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-5">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
|
||||
Extras
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<!-- <div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_0_check" type="checkbox" ${ports_data[0].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">External Port</label>
|
||||
<input type="text" class="form-control" name="port_0_external" value="${ports_data[0].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Internal Port</label>
|
||||
<input type="text" class="form-control" name="port_0_internal" value="${ports_data[0].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">Protocol</label>
|
||||
<select class="form-select" name="port_0_protocol">
|
||||
<option value="${ports_data[0].protocol}" selected hidden>${ports_data[0].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_1_check" type="checkbox" ${ports_data[1].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_1_external" value="${ports_data[1].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_1_internal" value="${ports_data[1].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_1_protocol">
|
||||
<option value="${ports_data[1].protocol}" selected hidden>${ports_data[1].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_2_check" type="checkbox" ${ports_data[2].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_2_external" value="${ports_data[2].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_2_internal" value="${ports_data[2].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_2_protocol">
|
||||
<option value="${ports_data[2].protocol}" selected hidden>${ports_data[2].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_3_check" type="checkbox" ${ports_data[3].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_3_external" value="${ports_data[3].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_3_internal" value="${ports_data[3].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_3_protocol">
|
||||
<option value="${ports_data[3].protocol}" selected hidden>${ports_data[3].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port_4_check" type="checkbox" ${ports_data[4].check}>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_4_external" value="${ports_data[4].external}"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_4_internal" value="${ports_data[4].internal}"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_4_protocol">
|
||||
<option value="${ports_data[4].protocol}" selected hidden>${ports_data[4].protocol}</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="" type="checkbox" >
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">External Port</label>
|
||||
<input type="text" class="form-control" name="" value=""/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Internal Port</label>
|
||||
<input type="text" class="form-control" name="" value=""/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">Protocol</label>
|
||||
<select class="form-select" name="">
|
||||
<option value="" selected hidden></option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
20
views/modals/import.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="modal-title">Import Template(s)</div>
|
||||
<div class="text-muted mb-2">Individual template(s) can be *.json, *.yml, or *.yaml.</div>
|
||||
<div class="text-muted mb-3">Multiple compose files can be imported from a zip file. Each folder should contain a single compose.yaml.</div>
|
||||
<img src = "/img/add to zip.jpg" alt = "Add to zip" class = "img-fluid" />
|
||||
<div class="mt-3">
|
||||
<form method="post" action="/upload" enctype="multipart/form-data" id="upload">
|
||||
<input class="form-control" type="file" name="files" multiple />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" data-bs-dismiss="modal" form="upload">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
728
views/modals/json.html
Normal file
|
@ -0,0 +1,728 @@
|
|||
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Install AppName</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<pre class="text-secondary">AppNote</pre>
|
||||
|
||||
<form action="/install" name="FormId_install" id="FormId_install" method="POST">
|
||||
|
||||
<div class="row mb-3 align-items-end">
|
||||
|
||||
<div class="col-lg-6">
|
||||
<label class="form-label">Container Name: </label>
|
||||
<input type="text" class="form-control" name="service_name" value="AppName" hidden/>
|
||||
<input type="text" class="form-control" name="name" value="AppName"/>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label">Image: </label>
|
||||
<input type="text" class="form-control" name="image" value="AppImage"/>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<label class="form-label">Restart Policy: </label>
|
||||
<select class="form-select" name="restart_policy">
|
||||
<option value="RestartPolicy" selected hidden>RestartPolicy</option>
|
||||
<option value="unless-stopped">unless-stopped</option>
|
||||
<option value="on-failure">on-failure</option>
|
||||
<option value="no">never</option>
|
||||
<option value="always">always</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="form-label">Network Mode</label>
|
||||
<div class="form-selectgroup-boxes row mb-3">
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="net_mode" value="host" class="form-selectgroup-input" NetHost>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Host Network</span>
|
||||
<span class="d-block text-secondary">Same as host. No isolation. ex.127.0.0.1</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="net_mode" value="NetName" class="form-selectgroup-input" NetBridge>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Bridge Network</span>
|
||||
<span class="d-block text-secondary">Containers can communicate using names.</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-selectgroup-item">
|
||||
<input type="radio" name="net_mode" value="docker" class="form-selectgroup-input" NetDocker>
|
||||
<span class="form-selectgroup-label d-flex align-items-center p-3">
|
||||
<span class="me-3">
|
||||
<span class="form-selectgroup-check"></span>
|
||||
</span>
|
||||
<span class="form-selectgroup-label-content">
|
||||
<span class="form-selectgroup-title strong mb-1">Docker Network</span>
|
||||
<span class="d-block text-secondary">Isolated on the docker network. ex.172.0.34.2</span>
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion" id="ModalName-accordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-1">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-1" aria-expanded="false">
|
||||
Ports
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-1" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port0" type="checkbox" Port0Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">External Port</label>
|
||||
<input type="text" class="form-control" name="port_0_external" value="Port0External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Internal Port</label>
|
||||
<input type="text" class="form-control" name="port_0_internal" value="Port0Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">Protocol</label>
|
||||
<select class="form-select" name="port_0_protocol">
|
||||
<option value="Port0Protocol" selected hidden>Port0Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port1" type="checkbox" Port1Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_1_external" value="Port1External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_1_internal" value="Port1Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_1_protocol">
|
||||
<option value="Port1Protocol" selected hidden>Port1Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port2" type="checkbox" Port2Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_2_external" value="Port2External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_2_internal" value="Port2Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_2_protocol">
|
||||
<option value="Port2Protocol" selected hidden>Port2Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port3" type="checkbox" Port3Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_3_external" value="Port3External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_3_internal" value="Port3Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_3_protocol">
|
||||
<option value="Port3Protocol" selected hidden>Port3Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port4" type="checkbox" Port4Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_4_external" value="Port4External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_4_internal" value="Port4Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_4_protocol">
|
||||
<option value="Port4Protocol" selected hidden>Port4Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="port5" type="checkbox" Port5Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_5_external" value="Port5External"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="port_5_internal" value="Port5Internal"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="port_5_protocol">
|
||||
<option value="Port5Protocol" selected hidden>Port5Protocol</option>
|
||||
<option value="tcp">tcp</option>
|
||||
<option value="udp">udp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-2">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-2" aria-expanded="false">
|
||||
Volumes
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-2" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume0" type="checkbox" Volume0Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_0_bind" value="Volume0Bind"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_0_container" value="Volume0Container"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_0_readwrite">
|
||||
<option value="Volume0RW" selected hidden>Volume0RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume1" type="checkbox" Volume1Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_1_bind" value="Volume1Bind"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_1_container" value="Volume1Container"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_1_readwrite">
|
||||
<option value="Volume1RW" selected hidden>Volume1RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume2" type="checkbox" Volume2Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_2_bind" value="Volume2Bind"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_2_container" value="Volume2Container"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_2_readwrite">
|
||||
<option value="Volume2RW" selected hidden>Volume2RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume3" type="checkbox" Volume3Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_3_bind" value="Volume3Bind"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_3_container" value="Volume3Container"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_3_readwrite">
|
||||
<option value="Volume3RW" selected hidden>Volume3RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume4" type="checkbox" Volume4Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_4_bind" value="Volume4Bind"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_4_container" value="Volume4Container"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_4_readwrite">
|
||||
<option value="Volume4RW" selected hidden>Volume4RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="volume5" type="checkbox" Volume5Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_5_bind" value="Volume5Bind"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="volume_5_container" value="Volume5Container"/>
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<select class="form-select" name="volume_5_readwrite">
|
||||
<option value="Volume5RW" selected hidden>Volume5RW</option>
|
||||
<option value="rw">rw</option>
|
||||
<option value="ro">ro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-3">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-3" aria-expanded="false">
|
||||
Environment Variables
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-3" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env0" Env0Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Variable</label>
|
||||
<input type="text" class="form-control" name="env_0_name" value="Env0Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" name="env_0_default" value="Env0Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env1" Env1Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_1_name" value="Env1Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_1_default" value="Env1Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env2" Env2Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_2_name" value="Env2Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_2_default" value="Env2Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env3" Env3Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_3_name" value="Env3Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_3_default" value="Env3Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env4" Env4Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_4_name" value="Env4Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_4_default" value="Env4Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env5" Env5Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_5_name" value="Env5Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_5_default" value="Env5Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env6" Env6Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_6_name" value="Env6Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_6_default" value="Env6Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env7" Env7Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_7_name" value="Env7Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_7_default" value="Env7Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env8" Env8Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_8_name" value="Env8Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_8_default" value="Env8Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env9" Env9Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_9_name" value="Env9Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_9_default" value="Env9Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env10" Env10Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_10_name" value="Env10Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_10_default" value="Env10Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="env11" Env11Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_11_name" value="Env11Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="env_11_default" value="Env11Default"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-4">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-4" aria-expanded="false">
|
||||
Labels
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-4" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label0" Label0Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Variable</label>
|
||||
<input type="text" class="form-control" name="label_0_name" value="Label0Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" name="label_0_value" value="Label0Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label1" Label1Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_1_name" value="Label1Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_1_value" value="Label1Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label2" Label2Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_2_name" value="Label2Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_2_value" value="Label2Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label3" Label3Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_3_name" value="Label3Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_3_value" value="Label3Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label4" Label4Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_4_name" value="Label4Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_4_value" value="Label4Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label5" Label5Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_5_name" value="Label5Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_5_value" value="Label5Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label6" Label6Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_6_name" value="Label6Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_6_value" value="Label6Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label7" Label7Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_7_name" value="Label7Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_7_value" value="Label7Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label8" Label8Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_8_name" value="Label8Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_8_value" value="Label8Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label9" Label9Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_9_name" value="Label9Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_9_value" value="Label9Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label10" Label10Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_10_name" value="Label10Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_10_value" value="Label10Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" type="checkbox" name="label11" Label11Check>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_name" value="Label11Name"/>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text" class="form-control" name="label_11_value" value="Label11Value"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="heading-5">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-5" aria-expanded="false">
|
||||
Extras
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-5" class="accordion-collapse collapse" data-bs-parent="#ModalName-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="command_check" type="checkbox" CommandCheck>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Command</label>
|
||||
<input type="text" class="form-control" name="command" value="CommandValue"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-1 align-items-end">
|
||||
<div class="col-auto">
|
||||
<input class="form-check-input" name="privileged" type="checkbox" PrivilegedCheck>
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label">Privileged Mode</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
|
||||
<input type="submit" form="FormId_install" class="btn btn-success" value="Install"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
views/modals/learnmore.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="modal-title">AppName</div>
|
||||
<div>AppDesc</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Okay</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
22
views/modals/permissions.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">PermissionsTitle Permissions</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="accordion" id="modal-accordion">
|
||||
PermissionsList
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form id="reset_permissions">
|
||||
<input type="hidden" name="container" value="PermissionsContainer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" name="reset_permissions" value="reset_permissions" id="submit" hx-post="/updatePermissions" hx-trigger="click" hx-confirm="Are you sure you want to reset permissions for this container?">Reset</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
77
views/modals/uninstall.html
Normal file
|
@ -0,0 +1,77 @@
|
|||
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<div class="modal-status bg-danger"></div>
|
||||
<div class="modal-body text-center py-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon mb-2 text-danger icon-lg" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 9v2m0 4v.01"></path><path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path></svg>
|
||||
<h3>Uninstall AppName?</h3>
|
||||
<form action="/uninstall" id="AppName_uninstall" method="POST">
|
||||
<input type="text" class="form-control" name="service_name" value="AppName" hidden/>
|
||||
<div class="mb-3"> </div>
|
||||
|
||||
<div class="mb-2">
|
||||
<div class="divide-y">
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">Remove Volumes</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_volumes" disabled="">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Remove Image
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_image" disabled="">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Remove Backups
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="remove_backups" disabled="">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1"> </div>
|
||||
<div class="text-muted">Enter "Yes" below to remove the container.</div>
|
||||
<input type="text" class="form-control mb-2" name="confirm" autocomplete="off">
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="w-100">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="#" class="btn w-100" data-bs-dismiss="modal">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="submit" form="AppName_uninstall" class="btn btn-danger w-100" value="Uninstall"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,188 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Networks</title>
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
<div class="col-12 mt-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Docker Networks</h3>
|
||||
<div class="card-options btn-list">
|
||||
<a href="#" class="btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
|
||||
Refresh
|
||||
</a>
|
||||
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#not_add-site">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
|
||||
New Network
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal modal-blur fade" id="add-site" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
|
||||
<form action="/addsite" id="addsite" method="POST">
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Type</div>
|
||||
<select class="form-select" name="type">
|
||||
<option value="reverse_proxy">Reverse Proxy</option>
|
||||
<option value="proxy">Proxy</option>
|
||||
<option value="file_server">File Server</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Domain / Subdomain</label>
|
||||
<input type="text" class="form-control" name="domain" placeholder="media.mydomainname.com">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="row g-2">
|
||||
<div class="col-8">
|
||||
<label class="form-label">Hostname / Host IP</label>
|
||||
<input type="text" class="form-control" name="host" placeholder="localhost">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label">Port</label>
|
||||
<input type="text" class="form-control" name="port" placeholder="8000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="divide-y">
|
||||
<div>
|
||||
<label class="row">
|
||||
<span class="col" title="HTTP Strict Transport Security (HSTS) is a simple and widely supported standard to protect visitors by ensuring that their browsers always connect to a website over HTTPS.">HSTS</span>
|
||||
<span class="col-auto">
|
||||
<label class="form-check form-check-single form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="hsts" checked="" disabled="">
|
||||
</label>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Container</div>
|
||||
<select class="form-select" name="container" disabled="">
|
||||
<option value="0" selected></option>
|
||||
<option value="1">Jellyfin</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
|
||||
<input type="submit" form="addsite" class="btn btn-success" value="Add"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="table-default" class="table-responsive">
|
||||
<table class="table">
|
||||
|
||||
<%- network_list %>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
|
||||
<span class="dropdown">
|
||||
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item" type="submit" formaction="/enablesite">
|
||||
Enable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/disablesite">
|
||||
Disable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/removesite">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
</form>
|
||||
|
||||
<p class="m-0 text-muted ms-auto"><%- network_count %> Networks</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Libs JS -->
|
||||
<script src="/libs/list.js/dist/list.min.js" defer></script>
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const list = new List('table-default', {
|
||||
sortClass: 'table-sort',
|
||||
listClass: 'table-tbody',
|
||||
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
|
||||
{ attr: 'data-date', name: 'sort-date' },
|
||||
{ attr: 'data-progress', name: 'sort-progress' },
|
||||
'sort-quantity'
|
||||
]
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function selectAll() {
|
||||
let checkboxes = document.getElementsByName('select');
|
||||
if (checkboxes[0].checked == true) {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = true;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
131
views/networks.html
Normal file
|
@ -0,0 +1,131 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Networks</title>
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('partials/navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
<div class="col-12 mt-12">
|
||||
<div class="card">
|
||||
<form method="post">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Docker Networks</h3>
|
||||
<div class="card-options btn-list">
|
||||
<!-- <a href="#" class="btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
|
||||
Refresh
|
||||
</a> -->
|
||||
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#not_add-site">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
|
||||
New Network
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="table-default" class="table-responsive">
|
||||
<table class="table">
|
||||
|
||||
<%- network_list %>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
|
||||
|
||||
<button class="btn" type="submit" formaction="/removeNetwork">Remove</button>
|
||||
|
||||
<!-- <span class="dropdown">
|
||||
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item" type="submit" formaction="/submitNetworks">
|
||||
Enable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/submitNetworks">
|
||||
Disable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/submitNetworks">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</span> -->
|
||||
|
||||
|
||||
|
||||
<p class="m-0 text-muted ms-auto"><%- network_count %> Networks</p>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Libs JS -->
|
||||
<script src="/libs/list.js/dist/list.min.js" defer></script>
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const list = new List('table-default', {
|
||||
sortClass: 'table-sort',
|
||||
listClass: 'table-tbody',
|
||||
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
|
||||
{ attr: 'data-date', name: 'sort-date' },
|
||||
{ attr: 'data-progress', name: 'sort-progress' },
|
||||
'sort-quantity'
|
||||
]
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function selectAll() {
|
||||
let checkboxes = document.getElementsByName('select');
|
||||
if (checkboxes[0].checked == true) {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = true;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
22
views/partials/appCard.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<div class="col-md-6 col-lg-3">
|
||||
<div class="card">
|
||||
<div class="card-body text-center">
|
||||
<span class="avatar avatar-xlplus mb-3 rounded"><img src='AppLogo' width="144px" height="144px" loading="lazy"/></span>
|
||||
<h3 class="m-0 mb-1"><a href="#">AppShortName</a></h3>
|
||||
<div class="text-secondary">AppDesc</div>
|
||||
<div class="mt-3">
|
||||
AppCategories
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<a href="#" class="card-btn" name="AppName" id="AppType" data-hx-get="/learn_more" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-article" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path> <path d="M7 8h10"></path> <path d="M7 12h10"></path> <path d="M7 16h10"></path></svg>
|
||||
Learn More
|
||||
</a>
|
||||
<a href="" class="card-btn" name="AppName" id="AppType" data-hx-get="/install_modal" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-bar-to-down" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M4 20l16 0"></path> <path d="M12 14l0 -10"></path> <path d="M12 14l4 -4"></path> <path d="M12 14l-4 -4"></path></svg>
|
||||
Install
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
80
views/partials/containerFull.html
Normal file
|
@ -0,0 +1,80 @@
|
|||
<div class="col-sm-6 col-lg-3 pt-1" hx-post="/dashboard/card" hx-trigger="sse:AppName" hx-swap="outerHTML" name="AppName">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-stamp card-stamp-sm">
|
||||
<img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/AppIcon.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader text-yellow">ExternalPort:InternalPort</div>
|
||||
<div class="ms-auto lh-1">
|
||||
<div class="card-actions btn-actions">
|
||||
<div class="card-actions btn-actions">
|
||||
<button class="btn-action" title="Start" data-hx-post="/dashboard/start" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
|
||||
</button>
|
||||
<button class="btn-action" title="Stop" data-hx-post="/dashboard/stop" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
|
||||
</button>
|
||||
<button class="btn-action" title="Pause" data-hx-post="/dashboard/pause" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
|
||||
</button>
|
||||
<button class="btn-action" title="Restart" data-hx-post="/dashboard/restart" data-hx-trigger="mousedown" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item text-secondary" name="AppName" id="details" data-hx-post="/dashboard/details" hx-swap="innerHTML" data-hx-trigger="mousedown" data-hx-target="#modals-here" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
|
||||
<button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-post="/dashboard/logs" hx-swap="innerHTML" hx-trigger="mousedown" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
|
||||
<button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
|
||||
<button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
|
||||
<button class="dropdown-item text-danger" name="AppName" id="uninstall" hx-trigger="mousedown" data-hx-post="/dashboard/uninstall" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/hide" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="hide" value="hide">Hide</button>
|
||||
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/reset" data-hx-trigger="mousedown" data-hx-swap="none" name="AppName" id="reset" value="reset">Reset View</button>
|
||||
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/permissions" name="AppName" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="mousedown" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline">
|
||||
<div class="h1 me-2" title="AppName" style="margin-bottom: 0;">
|
||||
<a href="http://${link}:${external_port}" target="_blank">
|
||||
AppShortName
|
||||
</a>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<label id="AppNameState">
|
||||
<span class="text-StateColor align-items-center lh-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
|
||||
AppState
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var ChartNamechart = new ApexCharts(document.querySelector("#ChartName_chart"), options);
|
||||
</script>
|
||||
|
||||
<div class="chart-sm">
|
||||
<div id="ChartName_chart" data-trigger="" data-hx-get="/chart" name="ChartName" hx-swap="innerHTML">
|
||||
<script>
|
||||
ChartNamechart.render();
|
||||
</script>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
71
views/partials/containerSimple.html
Normal file
|
@ -0,0 +1,71 @@
|
|||
<div class="col-sm-6 col-lg-3 pt-1" hx-get="" hx-trigger="" hx-swap="none" name="AppName">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="card-stamp card-stamp-sm">
|
||||
<img width="100px" src="https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/AppIcon.png" onerror="this.onerror=null;this.src='https://raw.githubusercontent.com/lllllllillllllillll/DweebUI-Icons/main/docker.png';"></img>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="subheader text-yellow">ExternalPort:InternalPort</div>
|
||||
<div class="ms-auto lh-1">
|
||||
<div class="card-actions btn-actions">
|
||||
<div class="card-actions btn-actions">
|
||||
<button class="btn-action" title="Start" data-hx-post="/dashboard/start" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-play" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 4v16l13 -8z"></path></svg>
|
||||
</button>
|
||||
<button class="btn-action" title="Stop" data-hx-post="/dashboard/stop" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-stop" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 2a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2z"></path></svg>
|
||||
</button>
|
||||
<button class="btn-action" title="Pause" data-hx-post="/dashboard/pause" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-player-pause" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path></svg>
|
||||
</button>
|
||||
<button class="btn-action" title="Restart" data-hx-post="/dashboard/restart" data-hx-trigger="click" data-hx-target="#AppNameState" name="AppName" id="AppState" ${disable}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-reload" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path><path d="M20 4v5h-5"></path></svg>
|
||||
</button>
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><circle cx="12" cy="12" r="1"></circle><circle cx="12" cy="19" r="1"></circle><circle cx="12" cy="5" r="1"></circle></svg>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item text-secondary" name="AppName" id="details" data-hx-get="/dashboard/modals" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Details</button>
|
||||
<button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-get="/dashboard/logs" hx-swap="innerHTML" data-hx-target="#logView" data-bs-toggle="modal" data-bs-target="#log_view">Logs</button>
|
||||
<button class="dropdown-item text-secondary" name="AppName" id="edit">Edit</button>
|
||||
<button class="dropdown-item text-primary" name="AppName" id="update" disabled="">Update</button>
|
||||
<button class="dropdown-item text-danger" name="AppName" id="uninstall" hx-trigger="click" data-hx-get="/modals" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<a href="#" class="btn-action dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-eye" width="24" height="24" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /> </svg>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/hide" data-hx-trigger="click" data-hx-swap="none" name="AppName" id="hide" value="hide">Hide</button>
|
||||
<button class="dropdown-item text-secondary" data-hx-post="/dashboard/reset" data-hx-trigger="click" data-hx-swap="none" name="AppName" id="reset" value="reset">Reset View</button>
|
||||
<button class="dropdown-item text-secondary" name="AppName" id="permissions" data-hx-get="/modals" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-baseline">
|
||||
<div class="h1 me-2" title="AppName">
|
||||
<a href="http://${link}:${external_port}" target="_blank">
|
||||
AppShortName
|
||||
</a>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<label id="AppNameState">
|
||||
<span class="text-StateColor align-items-center lh-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon-tabler icon-tabler-point-filled" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z" stroke-width="0" fill="currentColor"></path></svg>
|
||||
AppState
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -7,8 +7,7 @@
|
|||
<li class="list-inline-item"><a href="https://github.com/lllllllillllllillll/DweebUI/blob/main/LICENSE" class="link-secondary">License</a></li>
|
||||
<li class="list-inline-item"><a href="https://github.com/lllllllillllllillll/DweebUI/tree/main" target="_blank" class="link-secondary" rel="noopener">Source code</a></li>
|
||||
<li class="list-inline-item">
|
||||
<a href="https://github.com/lllllllillllllillll/DweebUI/tree/main" target="_blank" class="link-secondary" rel="noopener">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/heart -->
|
||||
<a href="https://www.buymeacoffee.com/lllllllillllllillll" target="_blank" class="link-secondary" rel="noopener">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-pink icon-filled icon-inline" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" /></svg>
|
||||
Sponsor
|
||||
</a>
|
||||
|
@ -23,8 +22,8 @@
|
|||
All rights reserved.
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href="#" class="link-secondary" rel="noopener">
|
||||
v0.10
|
||||
<a href="https://github.com/lllllllillllllillll/DweebUI/releases" class="link-secondary" rel="noopener">
|
||||
v0.60
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
|
@ -27,21 +27,30 @@
|
|||
}
|
||||
}
|
||||
</script>
|
||||
<header class="navbar navbar-expand-md d-print-none">
|
||||
<header class="navbar navbar-expand-md d-print-none py-0">
|
||||
<div class="container-xl">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu"
|
||||
aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0">
|
||||
<a href="#">
|
||||
<img src="/static/logo.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
|
||||
<img src="/img/logo.png" alt="DweebUI" title="DweebUI" height="40px">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img src="/img/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
<% if(alert) { %>
|
||||
<%- alert %>
|
||||
<% } %>
|
||||
|
||||
<div class="navbar-nav flex-row order-md-last">
|
||||
<div class="nav-item d-none d-md-flex me-3">
|
||||
|
||||
|
||||
|
||||
<!-- <div class="btn-list">
|
||||
<a href="#" class="btn text-green">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-lock" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6z"></path> <path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path> <path d="M8 11v-4a4 4 0 1 1 8 0v4"></path> </svg>
|
||||
|
@ -56,6 +65,16 @@
|
|||
VNC
|
||||
</a>
|
||||
</div> -->
|
||||
|
||||
<!-- <% if(role == 'admin') { %>
|
||||
<div class="btn-list">
|
||||
<a href="#" class="btn text-red">
|
||||
Admin
|
||||
</a>
|
||||
</div>
|
||||
<% } %> -->
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
@ -73,17 +92,21 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /> <path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /> </svg>
|
||||
</button>
|
||||
|
||||
<div class="nav-item dropdown d-none d-md-flex me-3">
|
||||
<div class="nav-item dropdown d-none d-md-flex me-2">
|
||||
<a href="#" class="nav-link px-0" data-bs-toggle="dropdown" tabindex="-1" aria-label="Show notifications">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/bell -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path d="M10 5a2 2 0 1 1 4 0a7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2 -3v-3a7 7 0 0 1 4 -6" /> <path d="M9 17v1a3 3 0 0 0 6 0v-1" /> </svg>
|
||||
<!-- <span class="badge bg-red"></span> -->
|
||||
</a>
|
||||
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-arrow dropdown-menu-end dropdown-menu-card">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Notifications</h3>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="list-group list-group-flush list-group-hoverable">
|
||||
<div class="list-group-item">
|
||||
<div class="row align-items-center">
|
||||
|
@ -120,14 +143,16 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item dropdown">
|
||||
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||
<span class="avatar avatar-sm"><%- avatar %></span>
|
||||
<span class="avatar avatar-sm bg-green-lt"><%= avatar %></span></span>
|
||||
<div class="d-none d-xl-block ps-2">
|
||||
<div>
|
||||
<%= name %>
|
||||
|
@ -138,11 +163,10 @@
|
|||
</div>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||
<!-- <a href="#" class="dropdown-item">Status</a> -->
|
||||
<a href="/account" class="dropdown-item">Account</a>
|
||||
<!-- <a href="#" class="dropdown-item">Feedback</a> -->
|
||||
<!-- <div class="dropdown-divider"></div> -->
|
||||
<a href="/settings" class="dropdown-item">Settings</a>
|
||||
<!-- <div class="dropdown-divider"></div> -->
|
||||
|
||||
<a href="/logout" class="dropdown-item">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -155,7 +179,7 @@
|
|||
<div class="container-xl">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/">
|
||||
<a class="nav-link" href="/dashboard">
|
||||
<span
|
||||
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/dashboard -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dashboard" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 13m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path> <path d="M13.45 11.55l2.05 -2.05"></path> <path d="M6.4 20a9 9 0 1 1 11.2 0z"></path> </svg>
|
||||
|
@ -239,17 +263,6 @@
|
|||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/portal">
|
||||
<span
|
||||
class="nav-link-icon d-md-none d-lg-inline-block"><!-- Download SVG icon from https://tabler-icons.io/i/user -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-star" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z" /></svg> </span>
|
||||
<span class="nav-link-title">
|
||||
Portal
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="my-2 my-md-0 flex-grow-1 flex-md-grow-0 order-first order-md-last">
|
13
views/partials/sidebar.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div class="col-3 d-none d-md-block border-end">
|
||||
<div class="card-body">
|
||||
<h4 class="subheader">Menu</h4>
|
||||
<div class="list-group list-group-transparent">
|
||||
<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center">Accounts</a>
|
||||
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center">Settings</a>
|
||||
</div>
|
||||
<h4 class="subheader mt-4">Other</h4>
|
||||
<div class="list-group list-group-transparent">
|
||||
<a href="/supporters" class="list-group-item list-group-item-action">Supporters</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
182
views/partials/user_permissions.html
Normal file
|
@ -0,0 +1,182 @@
|
|||
<div class="accordion-item mb-3" style="border: 1px solid grey;">
|
||||
<h2 class="accordion-header" id="heading-EntryNumber">
|
||||
<button class="accordion-button collapsed row" type="button" data-bs-toggle="collapse" data-bs-target="#collapse-EntryNumber" aria-expanded="false">
|
||||
<span class="avatar avatar-sm bg-green-lt col-3 text-start">JD</span>
|
||||
<div class="col text-end" style="margin-right: 10px;">PermissionsUsername</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse-EntryNumber" class="accordion-collapse collapse" data-bs-parent="#modal-accordion">
|
||||
<div class="accordion-body pt-0">
|
||||
|
||||
|
||||
<div class="">
|
||||
<div class="">
|
||||
<form id="updatePermissionsEntryNumber">
|
||||
<div class="row mb-3">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
All
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" onclick="selectAll('selectEntryNumber')">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="user" value="PermissionsUsername">
|
||||
<input type="hidden" name="container" value="PermissionsContainer">
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Uninstall
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="uninstall" data-UninstallCheck>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Edit
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="edit" data-EditCheck>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Upgrade
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="upgrade" data-UpgradeCheck>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Start
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="start" data-StartCheck>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Stop
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="stop" data-StopCheck>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Pause
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="pause" data-PauseCheck>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Restart
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="restart" data-RestartCheck>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-2">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
Logs
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="logs" data-LogsCheck>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-9">
|
||||
<label class="row text-start">
|
||||
<span class="col">
|
||||
View
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<label class="form-check form-check-single form-switch text-end">
|
||||
<input class="form-check-input" type="checkbox" name="selectEntryNumber" value="view" data-ViewCheck>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row mb-2">
|
||||
<button class="btn" type="button" id="submit" hx-post="/updatePermissions" hx-vals="#updatePermissionsEntryNumber" hx-swap="outerHTML">Update </button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
146
views/portal.ejs
|
@ -1,146 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Dashboard</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/meters.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
<div class="page-wrapper">
|
||||
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
<div class="col-12" id="cards">
|
||||
<div class="row row-cards">
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-primary text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="font-weight-medium">
|
||||
<label id="cpu-text" class="cpu-text mb-1" for="cpu">I had to put something here.</label>
|
||||
</div>
|
||||
<div id="cpu-bar" class="cpu-bar meter animate">
|
||||
<span style="width: 25%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-green text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="font-weight-medium">
|
||||
<label id="ram-text" class="ram-text mb-1" for="ram">RAM 0%</label>
|
||||
</div>
|
||||
<div id="ram-bar" class="ram-bar meter animate orange">
|
||||
<span style="width: 25%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-twitter text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="font-weight-medium">
|
||||
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
|
||||
</div>
|
||||
<div id="net-bar" class="meter animate blue">
|
||||
<span style="width: 25%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-facebook text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="font-weight-medium">
|
||||
<label id="disk-text" class="disk-text mb-1" for="disk">DISK 0%</label>
|
||||
</div>
|
||||
<div id="disk-bar" class="meter animate red">
|
||||
<span style="width: 25%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- inserted here from -->
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Libs JS -->
|
||||
<script src="/libs/apexcharts/dist/apexcharts.min.js" defer></script>
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
275
views/portal.html
Normal file
|
@ -0,0 +1,275 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Dashboard</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/meters.css" rel="stylesheet"/>
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/htmx-sse.js"></script>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
|
||||
<div class="page">
|
||||
|
||||
<%- include('partials/navbar.html') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards" hx-ext="sse" sse-connect="/sse_event">
|
||||
|
||||
<div class="col-12">
|
||||
<div class="row row-cards">
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-green text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-cpu" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 5m0 1a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1z"></path><path d="M9 9h6v6h-6z"></path><path d="M3 10h2"></path><path d="M3 14h2"></path><path d="M10 3v2"></path><path d="M14 3v2"></path><path d="M21 10h-2"></path><path d="M21 14h-2"></path><path d="M14 21v-2"></path><path d="M10 21v-2"></path></svg>
|
||||
</span>
|
||||
</div>
|
||||
<!-- HTMX -->
|
||||
<div class="col" name="CPU" id="green">
|
||||
<div class="font-weight-medium">
|
||||
<label class="cpu-text mb-1" for="cpu">CPU 0%</label>
|
||||
</div>
|
||||
<div class="cpu-bar meter animate green">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-blue text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-container" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 4v.01"></path> <path d="M20 20v.01"></path> <path d="M20 16v.01"></path> <path d="M20 12v.01"></path> <path d="M20 8v.01"></path> <path d="M8 4m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path> <path d="M4 4v.01"></path> <path d="M4 20v.01"></path> <path d="M4 16v.01"></path> <path d="M4 12v.01"></path> <path d="M4 8v.01"></path> </svg>
|
||||
</span>
|
||||
</div>
|
||||
<!-- HTMX -->
|
||||
<div class="col" name="RAM" id="blue">
|
||||
<div class="font-weight-medium">
|
||||
<label class="ram-text mb-1" for="ram">RAM 0%</label>
|
||||
</div>
|
||||
<div class="ram-bar meter animate blue">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-purple text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrows-left-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M21 17l-18 0"></path> <path d="M6 10l-3 -3l3 -3"></path> <path d="M3 7l18 0"></path> <path d="M18 20l3 -3l-3 -3"></path> </svg>
|
||||
</span>
|
||||
</div>
|
||||
<!-- HTMX -->
|
||||
<div class="col" name="NET" id="purple">
|
||||
<div class="font-weight-medium">
|
||||
<label id="net-text" class="net-text mb-1" for="network">Down: 0MB Up: 0MB</label>
|
||||
</div>
|
||||
<div class="ram-bar meter animate purple">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-6 col-lg-3">
|
||||
<div class="card card-sm">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto">
|
||||
<span class="bg-orange text-white avatar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-database" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"></path> <path d="M4 6v6a8 3 0 0 0 16 0v-6"></path> <path d="M4 12v6a8 3 0 0 0 16 0v-6"></path></svg>
|
||||
</span>
|
||||
</div>
|
||||
<!-- HTMX -->
|
||||
<div class="col" name="DISK" id="orange">
|
||||
<div class="font-weight-medium">
|
||||
<label class="disk-text mb-1" for="disk">DISK 0%</label>
|
||||
</div>
|
||||
<div class="meter animate orange">
|
||||
<span style="width:20%"><span></span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- HTMX -->
|
||||
<div class="col-12">
|
||||
<div class="row row-cards" id="containers" data-hx-get="/user_containers" data-hx-trigger="load" data-hx-swap="innerHTML">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTMX -->
|
||||
<div class="col-12">
|
||||
<div class="row row-cards" data-hx-get="/new_user_cards" data-hx-trigger="sse:update" data-hx-swap="afterbegin" hx-target="#containers">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTMX Target-->
|
||||
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Loading</h5>
|
||||
</div>
|
||||
<div class="modal-body text-center">
|
||||
<div class="spinner-border"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal modal-blur fade" id="log_view" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Logs</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body">
|
||||
<h4>Logs:</h4>
|
||||
<div id="logView">
|
||||
<pre>No logs available</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-info" onclick="viewLogs(this)" name="refresh">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/libs/apexcharts/dist/apexcharts.min.js"></script>
|
||||
<script src="/js/tabler.min.js"></script>
|
||||
<script>
|
||||
var options = {
|
||||
chart: {
|
||||
type: "line",
|
||||
height: 40.0,
|
||||
sparkline: {
|
||||
enabled: true
|
||||
},
|
||||
animations: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
fill: {
|
||||
opacity: 1
|
||||
},
|
||||
stroke: {
|
||||
width: [3, 1],
|
||||
dashArray: [0, 3],
|
||||
lineCap: "round",
|
||||
curve: "smooth"
|
||||
},
|
||||
series: [{
|
||||
name: "CPU",
|
||||
data: []
|
||||
}, {
|
||||
name: "RAM",
|
||||
data: []
|
||||
}],
|
||||
tooltip: {
|
||||
enabled: false
|
||||
},
|
||||
grid: {
|
||||
strokeDashArray: 4
|
||||
},
|
||||
xaxis: {
|
||||
labels: {
|
||||
padding: 0
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
yaxis: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
labels: {
|
||||
padding: 4
|
||||
}
|
||||
},
|
||||
colors: [tabler.getColor("primary"), tabler.getColor("gray-600")],
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- SelectAll for the permissions modal -->
|
||||
<script>
|
||||
function selectAll(group) {
|
||||
|
||||
let checkboxes = document.getElementsByName(group);
|
||||
if (checkboxes[0].checked == true) {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = true;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('fonts/inter.css');
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
@ -18,15 +18,23 @@
|
|||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class=" d-flex flex-column">
|
||||
<body class="d-flex flex-column">
|
||||
<script src="/js/demo-theme.js"></script>
|
||||
<div class="page page-center">
|
||||
|
||||
|
||||
<form class="container container-tight py-4" action="/register" method="POST" novalidate>
|
||||
|
||||
|
||||
<div class="text-center">
|
||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<img src="/img/logo.png" alt="DweebUI" title="DweebUI" height="100px">
|
||||
</h1>
|
||||
</div>
|
||||
<div class="text-center mb-4">
|
||||
<a href="#" class="navbar-brand navbar-brand-autodark d-none"><img src="/static/logo.svg" height="36" alt=""></a>
|
||||
<h1 class="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<img src="/img/dweebui.svg" alt="DweebUI" title="DweebUI" class="navbar-brand-image">
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
@ -81,84 +89,15 @@
|
|||
<input type="text" class="form-control" id="secret" name="secret">
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<label class="form-label">Avatar</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-6 col-sm-3">
|
||||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="rus.jpg" class="form-imagecheck-input" checked/>
|
||||
<span class="form-imagecheck-figure">
|
||||
<img src="img/avatars/rus.jpg" alt="Rich Uncle Skeleton" title="Rich Uncle Skeleton" class="form-imagecheck-image" width="100px">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3">
|
||||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="burns.jpg" class="form-imagecheck-input"/>
|
||||
<span class="form-imagecheck-figure">
|
||||
<img src="img/avatars/burns.jpg" alt="Montgomery Burns" title="Montgomery Burns" class="form-imagecheck-image" width="100px">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3">
|
||||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="frank.jpg" class="form-imagecheck-input" />
|
||||
<span class="form-imagecheck-figure">
|
||||
<img src="img/avatars/frank.jpg" alt="Frank Grimes" title= "Frank Grimes" class="form-imagecheck-image" width="100px">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3">
|
||||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="moe.jpg" class="form-imagecheck-input"/>
|
||||
<span class="form-imagecheck-figure">
|
||||
<img src="img/avatars/moe.jpg" alt="Moe Szyslak" title="Moe Szyslak" class="form-imagecheck-image" width="100px">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3">
|
||||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="poochie.jpg" class="form-imagecheck-input" />
|
||||
<span class="form-imagecheck-figure">
|
||||
<img src="img/avatars/poochie.jpg" alt="Poochie" title="Poochie" class="form-imagecheck-image" width="100px">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3">
|
||||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="skinner.jpg" class="form-imagecheck-input" />
|
||||
<span class="form-imagecheck-figure">
|
||||
<img src="img/avatars/skinner.jpg" alt="Seymour Skinner" title="Seymour Skinner" class="form-imagecheck-image" width="100px">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3">
|
||||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="moleman.png" class="form-imagecheck-input" />
|
||||
<span class="form-imagecheck-figure">
|
||||
<img src="img/avatars/moleman.png" alt="Hans Moleman" title="Hans Moleman" class="form-imagecheck-image" width="100px">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-3">
|
||||
<label class="form-imagecheck mb-2">
|
||||
<input name="avatar" type="radio" value="duffman.png" class="form-imagecheck-input" />
|
||||
<span class="form-imagecheck-figure">
|
||||
<img src="img/avatars/duffman.png" alt="Duffman" title="Duffman" class="form-imagecheck-image" width="100px">
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
|
||||
<!-- <div class="mb-2">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" class="form-check-input" name="warning"/>
|
||||
<span class="form-check-label">
|
||||
I understand that<a href="https://github.com/lllllllillllllillll/DweebUI/wiki/Exposing-DweebUI-to-the-Internet"> exposing DweebUI directly to the internet</a> is a bad idea.
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@ -176,16 +115,14 @@
|
|||
<div class="d-none d-md-flex">
|
||||
|
||||
<a href="?theme=dark" class="nav-link px-0 hide-theme-dark" title="Enable dark mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/moon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" /></svg>
|
||||
</a>
|
||||
<a href="?theme=light" class="nav-link px-0 hide-theme-light" title="Enable light mode" data-bs-toggle="tooltip" data-bs-placement="bottom">
|
||||
<!-- Download SVG icon from http://tabler-icons.io/i/sun -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" /><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Install</button>
|
||||
<button type="submit" class="btn btn-primary">Register</button>
|
||||
|
||||
</div>
|
||||
</div>
|
|
@ -1,136 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Settings</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Account Settings
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="card">
|
||||
<div class="row g-0">
|
||||
<div class="col-3 d-none d-md-block border-end">
|
||||
<div class="card-body">
|
||||
<h4 class="subheader">Business settings</h4>
|
||||
<div class="list-group list-group-transparent">
|
||||
<a href="/account" class="list-group-item list-group-item-action d-flex align-items-center">Account</a>
|
||||
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">My Notifications</a>
|
||||
<a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Connected Apps</a> -->
|
||||
<a href="/settings" class="list-group-item list-group-item-action d-flex align-items-center active">Settings</a>
|
||||
<!-- <a href="#" class="list-group-item list-group-item-action d-flex align-items-center">Billing & Invoices</a> -->
|
||||
</div>
|
||||
<h4 class="subheader mt-4">Experience</h4>
|
||||
<div class="list-group list-group-transparent">
|
||||
<a href="#" class="list-group-item list-group-item-action">Credits</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col d-flex flex-column">
|
||||
|
||||
<div class="card-body">
|
||||
<h2 class="mb-2">Settings</h2>
|
||||
<p class="text-muted mb-4">Configure server below</p>
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<a href="./QuickConnect.bat" class="btn" download="QuickConnect.bat">
|
||||
<!-- Download SVG icon from https://tabler-icons.io/i/brand-tabler-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-windows" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M17.8 20l-12 -1.5c-1 -.1 -1.8 -.9 -1.8 -1.9v-9.2c0 -1 .8 -1.8 1.8 -1.9l12 -1.5c1.2 -.1 2.2 .8 2.2 1.9v12.1c0 1.2 -1.1 2.1 -2.2 1.9z"></path> <path d="M12 5l0 14"></path> <path d="M4 12l16 0"></path> </svg>
|
||||
Windows QuickConnect
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md">
|
||||
<div class="form-label">Full Name</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">First Name</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Last Name</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Email</h3>
|
||||
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
|
||||
<div>
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<input type="text" class="form-control w-auto" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="btn">Change</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Password</h3>
|
||||
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
|
||||
<div>
|
||||
<a href="#" class="btn">
|
||||
Set new password
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Public profile</h3>
|
||||
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
|
||||
you.</p>
|
||||
<div>
|
||||
<label class="form-check form-switch form-switch-lg">
|
||||
<input class="form-check-input" type="checkbox" >
|
||||
<span class="form-check-label form-check-label-on">You're currently visible</span>
|
||||
<span class="form-check-label form-check-label-off">You're
|
||||
currently invisible</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
</body>
|
||||
</html>
|
120
views/settings.html
Normal file
|
@ -0,0 +1,120 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Settings</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('partials/navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Settings
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="card">
|
||||
<div class="row g-0">
|
||||
<%- include('partials/sidebar.html') %>
|
||||
<div class="col d-flex flex-column">
|
||||
|
||||
<div class="card-body">
|
||||
<h2 class="mb-2">Settings</h2>
|
||||
<p class="text-muted mb-4">Configure server below</p>
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<!-- <a href="./QuickConnect.bat" class="btn" download="QuickConnect.bat">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-brand-windows" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M17.8 20l-12 -1.5c-1 -.1 -1.8 -.9 -1.8 -1.9v-9.2c0 -1 .8 -1.8 1.8 -1.9l12 -1.5c1.2 -.1 2.2 .8 2.2 1.9v12.1c0 1.2 -1.1 2.1 -2.2 1.9z"></path> <path d="M12 5l0 14"></path> <path d="M4 12l16 0"></path> </svg>
|
||||
Windows QuickConnect
|
||||
</a> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md">
|
||||
<div class="form-label">Full Name</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">First Name</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Last Name</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Email</h3>
|
||||
<p class="card-subtitle">This contact will be shown to others publicly, so choose it carefully.</p>
|
||||
<div>
|
||||
<div class="row g-2">
|
||||
<div class="col-auto">
|
||||
<input type="text" class="form-control w-auto" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<a href="#" class="btn">Change</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Password</h3>
|
||||
<p class="card-subtitle">You can set a permanent password if you don't want to use temporary login codes.</p>
|
||||
<div>
|
||||
<a href="#" class="btn">
|
||||
Set new password
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="card-title mt-4">Public profile</h3>
|
||||
<p class="card-subtitle">Making your profile public means that anyone on the Dashkit network will be able to find
|
||||
you.</p>
|
||||
<div>
|
||||
<label class="form-check form-switch form-switch-lg">
|
||||
<input class="form-check-input" type="checkbox" >
|
||||
<span class="form-check-label form-check-label-on">You're currently visible</span>
|
||||
<span class="form-check-label form-check-label-off">You're
|
||||
currently invisible</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
</body>
|
||||
</html>
|
85
views/supporters.html
Normal file
|
@ -0,0 +1,85 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Settings</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('partials/navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Settings
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="card">
|
||||
<div class="row g-0">
|
||||
<%- include('partials/sidebar.html') %>
|
||||
<div class="col d-flex flex-column">
|
||||
|
||||
<div class="card-body">
|
||||
<h2 class="mb-2">Supporters</h2>
|
||||
<p class="text-muted mb-4">[Click to Thank]</p>
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
|
||||
<span type="button" class="avatar avatar-md bg-green-lt" hx-trigger="load, click" hx-post="/thank" hx-target="#count" name="MM" title="MM" style="margin-right: 5px;">mm</span>
|
||||
|
||||
<span type="button" class="avatar avatar-md bg-cyan-lt" hx-trigger="click" hx-post="/thank" hx-target="#count" name="PD" title="PD" style="margin-right: 5px;">pd</span>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-4">Thanks counter:</p>
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
|
||||
<span class="avatar avatar-md bg-yellow-lt" id="count" style="margin-right: 5px;">0</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
</body>
|
||||
</html>
|
|
@ -21,7 +21,7 @@
|
|||
<div class="page">
|
||||
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('partials/navbar.html') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
|
||||
|
@ -68,7 +68,7 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('partials/footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
|
@ -21,7 +21,7 @@
|
|||
<div class="page">
|
||||
|
||||
|
||||
<%- include('navbar.ejs') %>
|
||||
<%- include('partials/navbar.html') %>
|
||||
|
||||
<div class="page-wrapper">
|
||||
|
||||
|
@ -59,7 +59,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('footer.ejs') %>
|
||||
<%- include('partials/footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
120
views/variables.html
Normal file
|
@ -0,0 +1,120 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Settings</title>
|
||||
<!-- CSS files -->
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('partials/navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none">
|
||||
<div class="container-xl">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Settings
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="card">
|
||||
<div class="row g-0">
|
||||
<%- include('partials/sidebar.html') %>
|
||||
<div class="col d-flex flex-column">
|
||||
|
||||
<div class="card-body">
|
||||
<h2 class="mb-2">Variables</h2>
|
||||
<p class="text-muted mb-4">Configure default variables below.</p>
|
||||
|
||||
<!-- <div class="row align-items-center">
|
||||
<div class="col">
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md">
|
||||
<div class="form-label">Match 1</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Match 2</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Default</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md">
|
||||
<div class="form-label">Match 1</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Match 2</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Default</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md">
|
||||
<div class="form-label">Match 1</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Match 2</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-label">Default</div>
|
||||
<input type="text" class="form-control" value="" readonly="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/footer.html') %>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs JS -->
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,188 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Volumes</title>
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('navbar.ejs') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
<div class="col-12 mt-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Docker Volumes</h3>
|
||||
<div class="card-options btn-list">
|
||||
<a href="#" class="btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
|
||||
Refresh
|
||||
</a>
|
||||
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#not_add-site">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
|
||||
New Volume
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal modal-blur fade" id="add-site" tabindex="-1" style="display: none;" aria-hidden="true">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
|
||||
<form action="/addsite" id="addsite" method="POST">
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Type</div>
|
||||
<select class="form-select" name="type">
|
||||
<option value="reverse_proxy">Reverse Proxy</option>
|
||||
<option value="proxy">Proxy</option>
|
||||
<option value="file_server">File Server</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Domain / Subdomain</label>
|
||||
<input type="text" class="form-control" name="domain" placeholder="media.mydomainname.com">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="row g-2">
|
||||
<div class="col-8">
|
||||
<label class="form-label">Hostname / Host IP</label>
|
||||
<input type="text" class="form-control" name="host" placeholder="localhost">
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<label class="form-label">Port</label>
|
||||
<input type="text" class="form-control" name="port" placeholder="8000">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="divide-y">
|
||||
<div>
|
||||
<label class="row">
|
||||
<span class="col" title="HTTP Strict Transport Security (HSTS) is a simple and widely supported standard to protect visitors by ensuring that their browsers always connect to a website over HTTPS.">HSTS</span>
|
||||
<span class="col-auto">
|
||||
<label class="form-check form-check-single form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="hsts" checked="" disabled="">
|
||||
</label>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-label">Container</div>
|
||||
<select class="form-select" name="container" disabled="">
|
||||
<option value="0" selected></option>
|
||||
<option value="1">Jellyfin</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-link link-secondary me-auto" data-bs-dismiss="modal">Cancel</button>
|
||||
<input type="submit" form="addsite" class="btn btn-success" value="Add"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="table-default" class="table-responsive">
|
||||
<table class="table">
|
||||
|
||||
<%- volume_list %>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
|
||||
<span class="dropdown">
|
||||
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item" type="submit" formaction="/enablesite">
|
||||
Enable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/disablesite">
|
||||
Disable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/removesite">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
</form>
|
||||
|
||||
<p class="m-0 text-muted ms-auto"><%- volume_count %> Volumes</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('footer.ejs') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Libs JS -->
|
||||
<script src="/libs/list.js/dist/list.min.js" defer></script>
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const list = new List('table-default', {
|
||||
sortClass: 'table-sort',
|
||||
listClass: 'table-tbody',
|
||||
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
|
||||
{ attr: 'data-date', name: 'sort-date' },
|
||||
{ attr: 'data-progress', name: 'sort-progress' },
|
||||
'sort-quantity'
|
||||
]
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function selectAll() {
|
||||
let checkboxes = document.getElementsByName('select');
|
||||
if (checkboxes[0].checked == true) {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = true;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
163
views/volumes.html
Normal file
|
@ -0,0 +1,163 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>DweebUI - Volumes</title>
|
||||
<link href="/css/tabler.min.css" rel="stylesheet"/>
|
||||
<link href="/css/demo.min.css" rel="stylesheet"/>
|
||||
<style>
|
||||
@import url('/fonts/inter.css');
|
||||
:root {
|
||||
--tblr-font-sans-serif: 'Inter Var', -apple-system, BlinkMacSystemFont, San Francisco, Segoe UI, Roboto, Helvetica Neue, sans-serif;
|
||||
}
|
||||
body {
|
||||
font-feature-settings: "cv03", "cv04", "cv11";
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<div class="page">
|
||||
<!-- Navbar -->
|
||||
<%- include('partials/navbar.html') %>
|
||||
<div class="page-wrapper">
|
||||
<!-- Page header -->
|
||||
|
||||
<!-- Page body -->
|
||||
<div class="page-body">
|
||||
<div class="container-xl">
|
||||
<div class="row row-deck row-cards">
|
||||
|
||||
<div class="col-12 mt-12">
|
||||
<div class="card">
|
||||
<form method="post">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Docker Volumes</h3>
|
||||
<div class="card-options btn-list">
|
||||
<!-- <a href="#" class="btn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
|
||||
Refresh
|
||||
</a> -->
|
||||
<a href="#" class="btn" data-bs-toggle="modal" data-bs-target="#modals-here">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M12 5l0 14"></path> <path d="M5 12l14 0"></path> </svg>
|
||||
New Volume
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="table-default" class="table-responsive">
|
||||
<table class="table">
|
||||
|
||||
<%- volume_list %>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex align-items-center">
|
||||
|
||||
<button class="btn" type="submit" formaction="/removeVolume">Remove</button>
|
||||
|
||||
<!-- <span class="dropdown">
|
||||
<button class="btn dropdown-toggle align-text-top" data-bs-toggle="dropdown">Actions</button>
|
||||
<div class="dropdown-menu dropdown-menu-end">
|
||||
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
|
||||
Enable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
|
||||
Disable
|
||||
</button>
|
||||
<button class="dropdown-item" type="submit" formaction="/submitVolumes">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</span> -->
|
||||
|
||||
|
||||
<p class="m-0 text-muted ms-auto"><%- volume_count %> Volumes</p>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered modal-dialog-scrollables">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">New Volume</h5>
|
||||
</div>
|
||||
|
||||
<div class="modal-body text-center">
|
||||
<form method="post" action="/addVolume">
|
||||
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-9">
|
||||
<label class="form-label text-muted">Volume Name</label>
|
||||
<input type="text" class="form-control" name="volume">
|
||||
</div>
|
||||
|
||||
<div class="col-2">
|
||||
<button type="submit" class="btn mt-2">Create</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="mt-3 text-muted"><label class="text-danger">*</label>Name cannot contain spaces or special characters.</label>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%- include('partials/footer.html') %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Libs JS -->
|
||||
<script src="/libs/list.js/dist/list.min.js" defer></script>
|
||||
<!-- Tabler Core -->
|
||||
<script src="/js/tabler.min.js" defer></script>
|
||||
<script src="/js/demo.min.js" defer></script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const list = new List('table-default', {
|
||||
sortClass: 'table-sort',
|
||||
listClass: 'table-tbody',
|
||||
valueNames: [ 'sort-name', 'sort-type', 'sort-city', 'sort-score',
|
||||
{ attr: 'data-date', name: 'sort-date' },
|
||||
{ attr: 'data-progress', name: 'sort-progress' },
|
||||
'sort-quantity'
|
||||
]
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function selectAll() {
|
||||
let checkboxes = document.getElementsByName('select');
|
||||
if (checkboxes[0].checked == true) {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = true;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|