Skip to content

Commit 2d61b78

Browse files
RTLRTL
RTL
authored and
RTL
committed
Adding HTMX powered Light-Dashboard
1 parent 1d981a8 commit 2d61b78

21 files changed

+1027
-4
lines changed

Light-Dashboard/README.md

+11-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ devices.
66

77
![Light Dashboard Template](https://makoserver.net/blogmedia/dashboard/Light-Dashboard.gif)
88

9-
## Tutorial and hands-on microcontroller example:
10-
The server-side logic and web rendering are explained in the [dashboard article](https://makoserver.net/articles/How-to-Build-an-Interactive-Dashboard-App). For a hands-on microcontroller example of how to use this dashboard, see the tutorial, [Designing Your First Professional Embedded Web Interface](https://realtimelogic.com/articles/Designing-Your-First-Professional-Embedded-Web-Interface).
9+
10+
## Tutorial and Hands-On Microcontroller Example
11+
- Explore the server-side logic, HTMX, and web rendering principles detailed in the [dashboard article](https://makoserver.net/articles/How-to-Build-an-Interactive-Dashboard-App).
12+
- For a practical microcontroller implementation using this dashboard, check out the tutorial [Designing Your First Professional Embedded Web Interface](https://realtimelogic.com/articles/Designing-Your-First-Professional-Embedded-Web-Interface).
1113

1214
## How to run using the Mako Server:
1315
Run the dashboard example, using the [Mako Server](https://makoserver.net/), as follows:
@@ -16,6 +18,13 @@ Run the dashboard example, using the [Mako Server](https://makoserver.net/), as
1618
cd Light-Dashboard
1719
mako -l::www
1820
```
21+
22+
Run the [HTMX](https://htmx.org/) version as follows:
23+
```
24+
cd Light-Dashboard
25+
mako -l::htmx
26+
```
27+
1928
For detailed instructions on starting the Mako Server, check out our [command line video tutorial](https://youtu.be/vwQ52ZC5RRg) and review the server's [command line options](https://realtimelogic.com/ba/doc/?url=Mako.html#loadapp) in our documentation.
2029

2130
After starting the Mako Server, use a browser and navigate to

Light-Dashboard/htmx/.config

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-- See: https://realtimelogic.com/articles/Mastering-Xedge-Application-Deployment-From-Installation-to-Creation
2+
return {
3+
autostart=true,
4+
name="Light-Dashboard",
5+
dirname=""
6+
}

Light-Dashboard/htmx/.lua/cms.lua

+216
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
-- Mini Content Management System (CMS) designed for the Pure.css dashboard.
2+
3+
-- All LSP applications have a predefined 'dir' object, which is a resrdr instance.
4+
-- Ref resrdr: https://realtimelogic.com/ba/doc/en/lua/lua.html#ba_create_resrdr
5+
-- The dir object is not set when running as an Xedge xlua app (not LSP enabled app)
6+
-- https://realtimelogic.com/ba/doc/en/Xedge.html#using
7+
if not dir then
8+
error"This application must run as an LSP enabled app"
9+
end
10+
11+
local fmt=string.format
12+
13+
-- Load module for reading and writing raw-files and json-files.
14+
-- Details: https://realtimelogic.com/ba/doc/en/lua/auxlua.html#rwfile
15+
local rw=require"rwfile"
16+
17+
-- Convert to closures so variables work in the cmsfunc() callback function
18+
-- Closure def: https://en.wikipedia.org/wiki/Closure_(computer_programming)
19+
local app,io,dir=app,app.io,app.dir
20+
21+
-- Set on 'dir' and used by function cmsfunc()
22+
local securityPolicies={
23+
["Content-Security-Policy"]= "default-src 'self'; script-src 'self' cdn.jsdelivr.net unpkg.com 'unsafe-inline'; style-src 'self' cdn.jsdelivr.net unpkg.com 'unsafe-inline'",
24+
["X-Content-Type-Options"]="nosniff",
25+
}
26+
-- Doc: https://realtimelogic.com/ba/doc/en/lua/lua.html#rsrdr_header
27+
dir:header(securityPolicies)
28+
29+
-- Takes the data content of an LSP page (HTML with LSP tags) as
30+
-- argument, converts the LSP page to Lua code, and compiles the Lua
31+
-- code. The compiled Lua code is returned as a function.
32+
-- Details: https://realtimelogic.com/ba/doc/en/lua/lua.html#ba_parselsp
33+
local function parseLspPage(name)
34+
local func
35+
local data,err=rw.file(io,name) -- Read file content
36+
if data then
37+
data,err = ba.parselsp(data)
38+
if data then
39+
local func
40+
-- Compile Lua code
41+
-- ref load: https://realtimelogic.com/ba/doc/luaref_index.html?url=pdf-load
42+
func,err = load(data,name,"t")
43+
if func then return func end
44+
end
45+
end
46+
-- Failed
47+
err = fmt("parsing or running %s failed: %s",name,err)
48+
trace(err)
49+
return function(_ENV) print(err) end
50+
end
51+
52+
-- Parse and cache the template page.
53+
local templatePage=parseLspPage(".lua/www/template.lsp")
54+
55+
56+
-- Load the JSON encoded menu
57+
local menuL=rw.json(io,".lua/menu.json")
58+
assert(menuL, "lua/menu.json parse error")
59+
60+
--Create a key/value version of the menu list, where key is the relative path name 'href'.
61+
local menuT={}
62+
for _,m in ipairs(menuL) do
63+
menuT[m.href]=m
64+
end
65+
66+
-- An LSP page expects a persistent table unique to each page. The
67+
-- pagesT is a table where the key is the relative path and the value
68+
-- is the LSP page's unique table.
69+
-- Details: https://realtimelogic.com/ba/doc/en/lua/lua.html#CMDE
70+
local pagesT={}
71+
72+
-- The directory callback function. This callback is called by the
73+
-- server when a resource is accessed. Argument env is the
74+
-- request/response environment table and relpath is the relative path.
75+
-- See the following for details on the request/response environment:
76+
-- https://realtimelogic.com/ba/doc/en/lua/lua.html#CMDE
77+
local function cmsfunc(_ENV, relpath, notInMenuOK)
78+
trace("hx-request:",request:header"hx-request" and "yes" or "no")
79+
local response=response -- e.g. = _ENV.response. Now faster.
80+
81+
-- Translate to (path/)index.html if only directory name is provided.
82+
if #relpath == 0 or relpath:find"/$" then
83+
relpath = relpath.."index.html"
84+
end
85+
86+
-- Do we have the requested page (must be in file menu.json)
87+
if not menuT[relpath] and not notInMenuOK then
88+
if not relpath:find(".html",-5,true) then
89+
return false -- Not a html page. Let default 404 handle this
90+
end
91+
trace("Not found",relpath) -- For debug purposes
92+
response:setstatus(404)
93+
relpath = "404.html"
94+
end
95+
96+
-- Fetch the LSP page's persistent page table. Create the table if
97+
-- the page has so far not been accessed.
98+
local pageT=pagesT[relpath]
99+
if not pageT then
100+
pageT={}
101+
pagesT[relpath]=pageT
102+
end
103+
104+
--Remove the following line and xrsp:finalize() if you do not want to
105+
--compress the response.
106+
local xrsp = response:setresponse() -- Activate compression
107+
response:setdefaultheaders()
108+
109+
local lspPage=parseLspPage(".lua/www/"..relpath)
110+
if request:header"hx-request" then
111+
lspPage(_ENV,relpath,io,pageT,app)
112+
else
113+
for k,v in pairs(securityPolicies) do response:setheader(k,v) end
114+
-- Make the following available to template.lsp
115+
-- Note, we explicitly use the _ENV tab for code readability.
116+
-- Details: https://realtimelogic.com/ba/doc/en/lua/man/manual.html#2.2
117+
-- https://realtimelogic.com/ba/doc/en/lua/lua.html#CMDE
118+
_ENV.menuL=menuL
119+
_ENV.menuT=menuT
120+
_ENV.relpath=relpath
121+
-- lspPage is the parsed page (function as explained in parseLspPage above)
122+
_ENV.lspPage=lspPage
123+
-- Call the template page and pass in the required arguments.
124+
-- Arg details: https://realtimelogic.com/ba/doc/en/lua/lua.html#ba_parselsp
125+
templatePage(_ENV,relpath,io,pageT,app)
126+
-- non cached version of above. Use if testing new template.
127+
--parseLspPage(".lua/www/template.lsp")(_ENV,relpath,io,pageT,app)
128+
end
129+
xrsp:finalize(true) -- Send compressed data to client
130+
131+
return true
132+
end
133+
134+
-- Create the directory function used by our mini CMS system and
135+
-- install the callback function. A directory with no name is in
136+
-- effect a sibling when installed as a sub-directory. The following
137+
-- construction makes sure we do not trigger the callback for any
138+
-- static assets. In other words, static assets take precedence. The
139+
-- callback is called when no static asset is found.
140+
-- Ref dir: https://realtimelogic.com/ba/doc/en/lua/lua.html#ba_create_dir
141+
local cmsDir = ba.create.dir()
142+
cmsDir:setfunc(cmsfunc)
143+
144+
145+
-- Insert cmsDir as 'dir' siblings. This means the parent will be
146+
-- searched first. The parent resource reader (dir) manages the static
147+
-- content. See www/static for content returned to browser
148+
dir:insert(cmsDir,true)
149+
150+
151+
152+
----------------------- AUTHENTICATION -----------------------------
153+
154+
if ba.tpm then
155+
-- The following code is based on example from:
156+
-- https://realtimelogic.com/ba/doc/en/lua/auxlua.html#TPM
157+
158+
local cfgio = ba.openio"home" or ba.openio"disk" -- mako or xedge
159+
local rw=require"rwfile"
160+
161+
-- Read/write encrypted db. Write if 'encdb' provided
162+
local function rwdb(encdb)
163+
trace(encdb and "Writing" or "Reading","userdb.encrypted")
164+
return rw.file(cfgio,"userdb.encrypted",encdb)
165+
end
166+
167+
-- ba.create.authenticator() callback function
168+
local function loginresponse(_ENV, authinfo)
169+
-- How _ENV is used: https://realtimelogic.com/ba/doc/en/lua/lua.html#CMDE
170+
_ENV.authinfo = authinfo -- Makes it possible for .login-form.lsp to use authinfo
171+
-- The following prints the content of the authinfo table
172+
trace("loginresponse", ba.json.encode(authinfo))
173+
cmsfunc(_ENV,"login.html", true) -- Let the CMS function emit the login page.
174+
end
175+
176+
-- Create the wrapper and make the encrypted DB global
177+
local tju=ba.tpm.jsonuser("dashboard",true)
178+
179+
local authDir=nil
180+
local function setOrRemoveAuth()
181+
if #tju.users() > 0 and not authDir then -- SET
182+
authDir = ba.create.dir(1) -- Set priority to a value greater than cmsDir.
183+
local authenticator=ba.create.authenticator(tju.getauth(),{response=loginresponse, type="form"})
184+
authDir:setauth(authenticator)
185+
dir:insert(authDir,true) -- Higher prio than cmsDir thus executes before
186+
trace"Installing authenticator"
187+
elseif #tju.users() == 0 and authDir then -- REMOVE
188+
authDir:unlink() -- Remove authenticator
189+
authDir=nil
190+
trace"Removing authenticator"
191+
end
192+
end
193+
194+
function setuser(name,pwd) -- Used by www/.lua/www/Users.html
195+
rwdb(tju.setuser(name,pwd))
196+
setOrRemoveAuth()
197+
end
198+
199+
local encdb=rwdb() -- Load the encryted DB, if any
200+
if encdb then
201+
-- Set the DB
202+
local ok,err=tju.setdb(encdb)
203+
if ok then
204+
setOrRemoveAuth()
205+
else
206+
trace("Authenticator not installed; User DB error:",err)
207+
end
208+
else
209+
trace"No user database; Authenticator not installed"
210+
end
211+
else
212+
trace"No TPM. Authenticator not installed"
213+
end
214+
215+
-- Return onunload handler
216+
return function() end -- Not used

Light-Dashboard/htmx/.lua/menu.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[
2+
{
3+
"name": "Home",
4+
"href": "index.html"
5+
},
6+
{
7+
"name": "HTML Form",
8+
"href": "form.html"
9+
},
10+
{
11+
"name": "WebSockets",
12+
"href": "WebSockets.html"
13+
},
14+
{
15+
"name": "Users",
16+
"href": "Users.html"
17+
},
18+
{
19+
"name": "Sign Out",
20+
"href": "logout.html",
21+
"auth": true, //Only show if authenticated. See template.lsp
22+
}
23+
]
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<div class="header">
2+
<h1>Whoops</h1>
3+
<h2>404 - Page Not Found</h2>
4+
</div>
5+
6+
<div class="content">
7+
8+
</div>
9+
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?lsp
2+
if not app.setuser then
3+
print"Authenticator not enabled; See www/.lua/cms.lua for details"
4+
return
5+
end
6+
7+
-- HTML Escaping: Transforming potentially dangerous characters into a safe format.
8+
local xssfilt=(function()
9+
local escSyms= {
10+
['&']="&amp;",
11+
['<']="&lt;",
12+
['>']="&gt;",
13+
['"']="&quot;",
14+
["'"]="&#x27;",
15+
['/']="&#x2F;"
16+
}
17+
local function escape(c) return escSyms[c] end
18+
return function(x) return x and x:gsub("[&<>\"'/]", escape) end
19+
end)()
20+
21+
-- Trim string at both ends
22+
local function trim(x)
23+
return x and x:gsub("^%s*(.-)%s*$", "%1")
24+
end
25+
26+
local username,password
27+
if "POST" == request:method() then
28+
local data=request:data()
29+
username,password=xssfilt(trim(data.username)),xssfilt(trim(data.password))
30+
if username and password then
31+
if 0 == #password then password=nil end -- remove
32+
app.setuser(username,password)
33+
response:setheader("Refresh", "3")
34+
end
35+
end
36+
37+
?>
38+
<div class="header">
39+
<h1><?lsp=username and (password and "User Added" or "User Removed") or "Users"?></h1>
40+
</div>
41+
<div class="content">
42+
<form class="pure-form pure-form-stacked" method="POST">
43+
<fieldset class="pure-form pure-form-stacked">
44+
<legend>Add/Remove Local User</legend>
45+
<label for="AuthName">Username</label>
46+
<input name="username" type="text" id="AuthName" placeholder="Enter a username" class="pure-input-1"/>
47+
<label for="AuthPassword">Password</label>
48+
<input name="password" type="password" id="AuthPassword" placeholder="Enter a password or leave it blank to remove the user" class="pure-input-1"/>
49+
<div class="pure-controls">
50+
<button id="AuthSave" class="pure-button pure-button-primary">Save</button>
51+
</div>
52+
</fieldset>
53+
</form>
54+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!--
2+
Slider documentation: https://roundsliderui.com/
3+
-->
4+
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/roundslider.min.css" rel="stylesheet" />
5+
<link href="/static/WebSockets.css" rel="stylesheet" />
6+
<script src="/rtl/jquery.js"></script>
7+
<script src="/rtl/smq.js"></script>
8+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/roundslider.min.js"></script>
9+
<script src="/static/WebSockets.js"></script>
10+
11+
<div class="header">
12+
<h1>WebSockets</h1>
13+
<h2>Real Time Slider Via WebSockets (SMQ)</h2>
14+
</div>
15+
16+
<div class="content">
17+
<div id="SliderContainer">
18+
<div id="Slider">Whoops, your browser has no access to the Internet!</div>
19+
</div>
20+
<p>The slider example shows how to send real time data via WebSockets. You should see the slider angle being printed in real time in the console when you move the slider using the mouse.</p>
21+
22+
<p>Instead of using raw WebSockets, the data is sent via the pub/sub protocol <a target="_blank" href="https://realtimelogic.com/ba/doc/?url=SMQ.html">SMQ</a>, which runs on top of WebSockets. SMQ simplifies communicating with multiple clients. Open this page in a <a target="_blank" href="WebSockets.html">separate browser window</a> and move the slider. You should see the slider in the other browser window being updated too.</p>
23+
24+
<p>The slider position is stored persistently on the server side and refreshing this page restores the slider angle position in the browser. The server side immediately publishes the angle position to any new client (browser) that connects to the server. Right click on this page, click Inspect, and click the Network tab to get an understanding of the browser's load sequence. You should see the persistent WebSocket connection on the Network tab after clicking the refresh button.</p>
25+
26+
<p>See our <a target="_blank" href="https://tutorial.realtimelogic.com/WebSockets.lsp">online tutorial : WebSockets</a> for a WebSockets introduction.</p>
27+
</div>

0 commit comments

Comments
 (0)