소스 검색

localized htmx. fixed images/volumes/networks
added hidden checkbox so forms always return an array

lllllllillllllillll 1 년 전
부모
커밋
13ee350bb2
12개의 변경된 파일512개의 추가작업 그리고 26개의 파일을 삭제
  1. 5 3
      controllers/images.js
  2. 4 5
      controllers/networks.js
  3. 9 0
      controllers/variables.js
  4. 14 11
      controllers/volumes.js
  5. 355 0
      public/js/htmx-sse.js
  6. 0 0
      public/js/htmx.min.js
  7. 2 0
      router/index.js
  8. 2 2
      views/dashboard.html
  9. 0 1
      views/register.html
  10. 2 3
      views/settings.html
  11. 1 1
      views/supporters.html
  12. 118 0
      views/variables.html

+ 5 - 3
controllers/images.js

@@ -16,6 +16,10 @@ export const Images = async function(req, res) {
             <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>
+        <!-- Hidden checkbox so that the form returns an array each time -->
+        <tr class="d-none">
+            <td><input class="form-check-input m-0 align-middle" name="select" value="on" type="checkbox" checked="" aria-label="Select"></td>
+        </tr>
     </thead>
     <tbody class="table-tbody">`
 
@@ -33,7 +37,7 @@ export const Images = async function(req, res) {
                 <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">${images[i].RepoTags}</td>
                 <td class="sort-city">${images[i].Id}</td>
-                <td class="sort-type">Latest</td>
+                <td class="sort-type"> - </td>
                 <td class="sort-score text-green"> - </td>
                 <td class="sort-date" data-date="1628122643">${created}</td>
                 <td class="sort-quantity">${size} MB</td>
@@ -58,7 +62,6 @@ export const Images = async function(req, res) {
 
 
 export const removeImage = async function(req, res) {
-    
     let images = req.body.select;
 
     console.log(images);
@@ -75,6 +78,5 @@ export const removeImage = async function(req, res) {
             }
         }
     }
-
     res.redirect("/images");
 }

+ 4 - 5
controllers/networks.js

@@ -15,6 +15,10 @@ export const Networks = async function(req, res) {
                 <th><button class="table-sort" data-sort="sort-date">Created</button></th>
                 <th><button class="table-sort" data-sort="sort-progress">Action</button></th>
             </tr>
+            <!-- Hidden checkbox so that the form returns an array each time -->
+            <tr class="d-none">
+                <td><input class="form-check-input m-0 align-middle" name="select" value="on" type="checkbox" checked="" aria-label="Select"></td>
+            </tr>
         </thead>
     <tbody class="table-tbody">`
 
@@ -38,7 +42,6 @@ export const Networks = async function(req, res) {
     
     network_list += `</tbody>`
 
-    
     res.render("networks", {
         name: req.session.user,
         role: req.session.role,
@@ -46,16 +49,13 @@ export const Networks = async function(req, res) {
         network_list: network_list,
         network_count: networks.length
     });
-
 }
 
 
 
 
 export const removeNetwork = async function(req, res) {
-    
     let networks = req.body.select;
-
     for (let i = 0; i < networks.length; i++) {
         
         if (networks[i] != 'on') {
@@ -68,6 +68,5 @@ export const removeNetwork = async function(req, res) {
             }
         }
     }
-
     res.redirect("/networks");
 }

+ 9 - 0
controllers/variables.js

@@ -0,0 +1,9 @@
+
+export const Variables = (req, res) => {
+
+    res.render("variables", {
+        name: req.session.user,
+        role: req.session.role,
+        avatar: req.session.avatar,
+    });
+}

+ 14 - 11
controllers/volumes.js

@@ -17,6 +17,10 @@ export const Volumes = async function(req, res) {
             <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>
+        <!-- Hidden checkbox so that the form returns an array each time -->
+        <tr class="d-none">
+            <td><input class="form-check-input m-0 align-middle" name="select" value="on" type="checkbox" checked="" aria-label="Select"></td>
+        </tr>
     </thead>
     <tbody class="table-tbody">`
 
@@ -50,7 +54,7 @@ export const Volumes = async function(req, res) {
             <td class="sort-score text-green"> - </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="#" disabled="">Details</a></td>
+            <td class="text-end"><a class="btn" href="#">Details</a></td>
         </tr>`
     
         volume_list += details;    
@@ -71,21 +75,20 @@ export const Volumes = async function(req, res) {
 
 
 
-
-
-
 export const removeVolume = async function(req, res) {
-    
     let volumes = req.body.select;
 
     for (let i = 0; i < volumes.length; i++) {
-        let volume = docker.getVolume(volumes[i]);
-        try {
-            volume.remove();
-        }   catch (error) {
-                console.log(`Unable to remove volume ${volumes[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");
 }

+ 355 - 0
public/js/htmx-sse.js

@@ -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;
+	}
+
+})();

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
public/js/htmx.min.js


+ 2 - 0
router/index.js

@@ -11,6 +11,7 @@ import { Images, removeImage } from "../controllers/images.js";
 import { Networks, removeNetwork } from "../controllers/networks.js";
 import { Volumes, removeVolume } from "../controllers/volumes.js";
 import { Account } from "../controllers/account.js";
+import { Variables } from "../controllers/variables.js";
 import { Settings } from "../controllers/settings.js";
 import { Supporters } from "../controllers/supporters.js";
 import { Syslogs } from "../controllers/syslogs.js";
@@ -55,6 +56,7 @@ router.get("/users", auth, Users);
 router.get("/syslogs", auth, Syslogs);
 
 router.get("/account", Account);
+router.get("/variables", auth, Variables);
 router.get("/settings", auth, Settings);
 router.get("/supporters", Supporters);
 

+ 2 - 2
views/dashboard.html

@@ -8,8 +8,8 @@
     <!-- CSS files -->
     <link href="/css/tabler.min.css" rel="stylesheet"/>
     <link href="/css/meters.css" rel="stylesheet"/>
-    <script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
-    <script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
+    <script src="/js/htmx.min.js"></script>
+    <script src="/js/htmx-sse.js"></script>
     <style>
       @import url('/fonts/inter.css');
       :root {

+ 0 - 1
views/register.html

@@ -8,7 +8,6 @@
     <!-- CSS files -->
     <link href="/css/tabler.min.css" rel="stylesheet"/>
     <link href="/css/demo.min.css" rel="stylesheet"/>
-    <script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
     <style>
       @import url('/fonts/inter.css');
       :root {

+ 2 - 3
views/settings.html

@@ -49,11 +49,10 @@
 							
 							<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-->
+								<!-- <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>
+								</a> -->
 								</div>
 							</div>
 							

+ 1 - 1
views/supporters.html

@@ -8,7 +8,7 @@
 		<!-- CSS files -->
 		<link href="/css/tabler.min.css" rel="stylesheet"/>
 		<link href="/css/demo.min.css" rel="stylesheet"/>
-		<script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script>
+		<script src="/js/htmx.min.js"></script>
 		<style>
 			@import url('/fonts/inter.css');
 			:root {

+ 118 - 0
views/variables.html

@@ -0,0 +1,118 @@
+	<!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.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('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">
+
+									
+								</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.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>

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.