refactoring dashboard.js / updated compose file

This commit is contained in:
lllllllillllllillll 2024-03-14 21:12:09 -07:00
parent c9da3bd30b
commit e78afb90ca
6 changed files with 103 additions and 190 deletions

View file

@ -7,7 +7,6 @@ import { currentLoad, mem, networkStats, fsSize } from 'systeminformation';
const permissionsModal = readFileSync('./views/modals/permissions.html', 'utf8');
const uninstallModal = readFileSync('./views/modals/uninstall.html', 'utf8');
const detailsModal = readFileSync('./views/modals/details.html', 'utf8');
// The actual page
@ -21,7 +20,6 @@ export const Dashboard = (req, res) => {
// Server metrics (CPU, RAM, TX, RX, DISK)
let [ cpu, ram, tx, rx, disk, stats ] = [0, 0, 0, 0, 0, {}];
let serverMetrics = setInterval(async () => {
currentLoad().then(data => {
cpu = Math.round(data.currentLoad);
@ -38,7 +36,6 @@ let serverMetrics = setInterval(async () => {
});
}, 1000);
export const Stats = async (req, res) => {
let name = req.header('hx-trigger-name');
let color = req.header('hx-trigger');
@ -64,13 +61,73 @@ export const Stats = async (req, res) => {
res.send(info);
}
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 details = {
name: containerName,
image: image,
service: image[image.length - 1].split(':')[0],
state: info.State.Status,
external_port: ports_list[0]?.external || 0,
internal_port: ports_list[0]?.internal || 0,
ports: ports_list,
link: 'localhost',
}
return details;
}
let [ hidden, cardList, eventType, containersArray, sentArray ] = [ '', '', '', [], [] ];
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;
}
// if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
let card = readFileSync('./views/partials/containerCard.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(/ChartName/g, details.name.replace(/-/g, ''));
card = card.replace(/AppNameState/g, `${details.name}State`);
card = card.replace(/data-trigger=""/, trigger);
return card;
}
// HTMX server-side events
let [ hidden, cardList, newCards, containersArray, sentArray, updatesArray ] = [ '', '', '', [], [], [] ];
let cardUpdates = [];
let newCards = '';
export const SSE = (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' });
@ -80,45 +137,49 @@ export const SSE = (req, res) => {
await docker.listContainers({ all: true }).then(containers => {
containers.forEach(container => {
let name = container.Names[0].replace('/', '');
if (hidden.includes(name)) {
// do nothing
} else {
if (!hidden.includes(name)) { // if not hidden
containersArray.push({ container: name, state: container.State });
}
}
});
});
if ((JSON.stringify(containersArray) !== JSON.stringify(sentArray))) {
cardList = '';
newCards = '';
cardUpdates = [];
containersArray.forEach(container => {
const { container: containerName, state } = container;
const existingContainer = sentArray.find(c => c.container === containerName);
if (!existingContainer) {
addCard(containerName, 'newCards');
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) {
cardUpdates.push(containerName);
updatesArray.push(containerName);
}
addCard(containerName, 'cardList');
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) {
cardUpdates.push(containerName);
updatesArray.push(containerName);
}
});
for (let i = 0; i < cardUpdates.length; i++) {
res.write(`event: ${cardUpdates[i]}\n`);
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();
}
@ -161,143 +222,21 @@ export const Installs = async (req, res) => {
}
async function containerInfo (containerName) {
let card = readFileSync('./views/partials/containerCard.html', 'utf8');
let container = docker.getContainer(containerName);
let info = await container.inspect();
let name = containerName;
let state = info.State.Status;
let state_color = '';
switch (state) {
case 'running':
state_color = 'green';
break;
case 'exited':
state_color = 'red';
state = 'stopped';
break;
case 'paused':
state_color = 'orange';
break;
}
let wrapped = name;
if (name.length > 13) { wrapped = name.slice(0, 10) + '...'; }
if (name.startsWith('dweebui')) { disable = 'disabled=""'; }
let disable = "";
let chartName = name.replace(/-/g, '');
let trigger = 'data-hx-trigger="load, every 3s"';
if (state != 'running') { trigger = 'data-hx-trigger="load"'; }
let imageVersion = info.Config.Image.split('/');
let service = imageVersion[imageVersion.length - 1].split(':')[0];
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 {}
let external_port = ports_list[0]?.external || 0;
let internal_port = ports_list[0]?.internal || 0;
card = card.replace(/AppName/g, name);
card = card.replace(/AppShortName/g, wrapped);
card = card.replace(/AppIcon/g, service);
card = card.replace(/AppState/g, state);
card = card.replace(/StateColor/g, state_color);
card = card.replace(/ChartName/g, chartName);
card = card.replace(/data-trigger=""/, trigger);
if (hidden.includes(name)) { card = ''; }
return card;
}
async function addCard(container, list) {
let card = readFileSync('./views/partials/containerCard.html', 'utf8');
let containerId = docker.getContainer(container);
containerId.inspect().then(data => {
let state = data.State.Status;
let wrapped = container;
let disable = "";
let chartName = container.replace(/-/g, '');
// shorten long names
if (container.length > 13) { wrapped = container.slice(0, 10) + '...'; }
// disable buttons for dweebui
if (container.startsWith('dweebui')) { disable = 'disabled=""'; }
// if ( external_port == undefined ) { external_port = 0; }
// if ( internal_port == undefined ) { internal_port = 0; }
let state_color = 'green';
if (state == 'exited') {
state = 'stopped';
state_color = 'red';
} else if (state == 'paused') {
state_color = 'orange';
}
let trigger = 'data-hx-trigger="load, every 3s"';
if (state != 'running') { trigger = 'data-hx-trigger="load"'; }
let imageVersion = data.Config.Image.split('/');
let service = imageVersion[imageVersion.length - 1].split(':')[0];
let ports_list = [];
try {
for (const [key, value] of Object.entries(data.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;
card = card.replace(/AppName/g, container);
card = card.replace(/AppShortName/g, wrapped);
card = card.replace(/AppIcon/g, service);
card = card.replace(/AppState/g, state);
card = card.replace(/StateColor/g, state_color);
card = card.replace(/ChartName/g, chartName);
card = card.replace(/AppNameState/g, `${container}State`);
card = card.replace(/data-trigger=""/, trigger);
if (list == 'newCards') {
newCards += card;
} else if (list == 'cardList'){
cardList += card;
}
});
}
export const updateCards = async (req, res) => {
console.log('updateCards called');
res.send(newCards);
}
export const Containers = async (req, res) => {
res.send(cardList);
}
export const Card = async (req, res) => {
let name = req.header('hx-trigger-name');
console.log(`Updated card for ${name}`);
let card = await containerInfo(name);
let details = await containerInfo(name);
let card = await createCard(details);
res.send(card);
}
@ -398,7 +337,7 @@ export const Reset = async (req, res) => {
res.send("ok");
}
export const Modal = async (req, res) => {
export const Modals = async (req, res) => {
let name = req.header('hx-trigger-name');
let id = req.header('hx-trigger');
@ -414,35 +353,10 @@ export const Modal = async (req, res) => {
return;
}
let containerId = docker.getContainer(name);
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 modal = readFileSync('./views/modals/details.html', 'utf8');
let details = await containerInfo(name);
let container_info = {
name: containerInfo.Name.slice(1),
state: containerInfo.State.Status,
image: containerInfo.Config.Image,
external_port: external_port,
internal_port: internal_port,
ports: ports_list,
link: 'localhost',
}
let details = detailsModal;
details = details.replace(/AppName/g, containerInfo.Name.slice(1));
details = details.replace(/AppImage/g, containerInfo.Config.Image);
res.send(details);
modal = modal.replace(/AppName/g, details.name);
modal = modal.replace(/AppImage/g, details.image);
res.send(modal);
}

View file

@ -7,7 +7,7 @@ services:
PORT: 8000
SECRET: MrWiskers
restart: unless-stopped
ports:
expose:
- 8000:8000
volumes:
- dweebui:/app

View file

@ -4,7 +4,7 @@ export const router = express.Router();
// Controllers
import { Login, submitLogin, Logout } from "../controllers/login.js";
import { Register, submitRegister } from "../controllers/register.js";
import { Dashboard, Start, Stop, Pause, Restart, Logs, Modal, Stats, Hide, Reset, Chart, Installs, SSE, Card, updateCards } from "../controllers/dashboard.js";
import { Dashboard, Start, Stop, Pause, Restart, Logs, Modals, Stats, Hide, Reset, Chart, Installs, SSE, Card, updateCards, Containers } from "../controllers/dashboard.js";
import { Apps, appSearch } from "../controllers/apps.js";
import { Users } from "../controllers/users.js";
import { Images, removeImage } from "../controllers/images.js";
@ -33,13 +33,14 @@ router.post("/stop", auth, Stop);
router.post("/pause", auth, Pause);
router.post("/restart", auth, Restart);
router.get("/logs", auth, Logs);
router.get ("/modal", auth, Modal);
router.get ("/modals", auth, Modals);
router.get("/stats", auth, Stats);
router.post("/hide", auth, Hide);
router.post("/reset", auth, Reset);
router.get("/chart", auth, Chart);
router.get("/installs", auth, Installs);
router.get("/sse_event", auth, SSE);
router.get("/containers", auth, Containers);
router.get("/card", auth, Card);
router.get("/new_cards", auth, updateCards);

View file

@ -30,9 +30,7 @@
<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 -->

View file

@ -142,13 +142,13 @@
<!-- HTMX -->
<div class="col-12">
<div class="row row-cards" id="containers">
<div class="row row-cards" id="containers" data-hx-get="/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_cards" data-hx-trigger="load, sse:update" data-hx-swap="beforeend" hx-target="#containers">
<div class="row row-cards" data-hx-get="/new_cards" data-hx-trigger="sse:update" data-hx-swap="beforeend" hx-target="#containers">
</div>
</div>

View file

@ -26,11 +26,11 @@
<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="/modal" 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="details" 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">Details</button>
<button class="dropdown-item text-secondary" name="AppName" id="logs" data-hx-get="/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="/modal" hx-swap="innerHTML" data-bs-toggle="modal" data-hx-target="#modals-here" data-bs-target="#modals-here">Uninstall</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">
@ -40,7 +40,7 @@
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-secondary" data-hx-post="/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="/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="/modal" data-hx-target="#modals-here" hx-swap="innerHTML" data-hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here">Permissions</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>
@ -48,7 +48,7 @@
</div>
</div>
<div class="d-flex align-items-baseline">
<div class="h1 me-2" title="AppName" style="margin-bottom: 0;">
<div class="h1 me-2" title="AppName">
<a href="http://${link}:${external_port}" target="_blank">
AppShortName
</a>