This post will guide you integrate Nexus Dashboard to Phoenix Web Framework. First of all, you should buy and use Nexus Dashboard instead of craking it!
I will assume that you did download and unzip the web dashboard template, The latest version is 2.2.0.
Step 1: Go to nexus directory, find css assets.
My nexus version is 2.2.0, after unzip, you will need to go to nexus-html@2.2.0/src/styles/.
All css assets here! The whole point now is to replace phoenix app.css to nexus app.css.
As you can see, they use import another css in relative paths. When we integrate it with phoenix, we need to take care those paths.
Step 2: Copy nexus css assets to phoenix web app
In phoenix web app, open directory /assets/css/ and create a new sub-directory named nexus_dashboard.
In this assets/css/nexus_dashboard, copy nexus css into this, except app.css.
This is a directory tree after copy for assets/css/nexus_dashboard
Now, come back the app.css, I don’t want a conflict with the existing one app.css, so I named a new main css file as nexus_app.css. In addition, due to different relative paths, I must update these with ./nexus_dashboard.
You gonna see a lot of missing javascrip library from now on, but I will show you how to solve it. At first, run mix tailwind _project_name.
100% you gonna see missing daisyui.js and daisyui-theme.js in assets/css/nexus_dashboard/daisyui.css. To solve it, you need to use relative path to assets/vendor/daisyui.js and assets/vendor/daisyui-theme.js. I would like to keep these file as is. When you create phoenix 1.8 project, daisyui.js and daisyui-theme.js is already there!.
In addition, you gonna see a mission iconify/tailwind4 library. At this point, there is no choice but using npm ecosystem. In assets, run npm install @iconify-json/lucide and @iconify/tailwind4.
This is content for assets/package.json. I really want to depend as less as possible the npm ecosystem, but have no choice.
A minor note for you, cause you need lucide-icon and rely on npm install to collect @iconify, lucide and tailwind4 for lucide-icon. it’s a good idea that you can ignore prebuild vendor/daisyui.js and daisyui-theme.js,
just use npm install these two libraries. Remember to update your nexus_dashboard/daisyui.css’s relative paths.
Step 2: Extract the zip file, and find a file named metadata.json
This is file content of metadata.json.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{"_generated":"Generated by SweetTooth, do not edit","description":"Adds AppIndicator, KStatusNotifierItem and legacy Tray icons support to the Shell","gettext-domain":"AppIndicatorExtension","name":"AppIndicator and KStatusNotifierItem Support","settings-schema":"org.gnome.shell.extensions.appindicator","shell-version":["45","46","47"],"url":"https://github.com/ubuntu/gnome-shell-extension-appindicator","uuid":"appindicatorsupport@rgcjonas.gmail.com","version":59}
Look at line 13, looking for the uuid, it’s appindicatorsupport@rgcjonas.gmail.com.
Step 3: Rename the extracted directory to the uuid that is copied in Step 2.
It’s appindicatorsupport@rgcjonas.gmail.com
Step 4: Copy the renamed directory in Step-3 to ~/.local/share/gnome-shell/extensions.
How to test?
You can go to terminal and type gnome-extensions list.
$ gnome-extensions list
appindicatorsupport@rgcjonas.gmail.com
apps-menu@gnome-shell-extensions.gcampax.github.com
background-logo@fedorahosted.org
launch-new-instance@gnome-shell-extensions.gcampax.github.com
places-menu@gnome-shell-extensions.gcampax.github.com
window-list@gnome-shell-extensions.gcampax.github.com
On the other hand, you can open gnome extensions - https://apps.gnome.org/Extensions/. Your new extension should be there in Manually installed list.
Thay đổi mining_rig_monitor_web.ex , phần live_view/0:
use Phoenix.LiveView, layout: {MiningRigMonitorWeb.Layouts, :app} -> use Phoenix.LiveView
Sau cùng, nó sẽ trông như thế này.
# Phiên bản cũdeflive_viewdoquotedousePhoenix.LiveView,layout:{MiningRigMonitorWeb.Layouts,:app}unquote(html_helpers())endend# Phiên bản mớideflive_viewdoquotedousePhoenix.LiveViewunquote(html_helpers())endend
Tôi đã nghĩ rằng sẽ phải có lỗi xảy ra ở lần chạy iex -S mix phx.server đầu tiên, tôi sẽ cần phải thay đổi các live_view liên quan, nhưng mà không, khả năng tương thích ngược là khá tốt.
Tôi trích dẫn một cái quan trọng liên quan đến root.html.heex và app.html.heex (version 1.7) , app/1 (version 1.8).
defmoduleDevAppWeb.Layoutsdo@moduledoc"""
This module holds different layouts used by your application.
See the `layouts` directory for all templates available.
The "root" layout is a skeleton rendered as part of the
application router. The "app" layout is rendered as component
in regular views and live views.
"""end
cái app/1 này phải được gọi thì nó mới render, không có chuyện chạy mặc định. Có thể ở 1.7 có file app.html.heex, tuy nhiên, nó ko liên quan đến func app/1
Tôi cần kiểm tra chéo 1 chút, thực sự là app.html.heex có được chạy mặc định hay không, sau khi tôi đã bỏ option layout: {MiningRigMonitorWeb.Layouts, :app}
Để test, tôi đã thêm 1 cái tag <h1> cho file app.html.heex để đánh dấu.
Đệt, nó ko chạy qua app.html.heex sau khi bỏ option layout: {MiningRigMonitorWeb.Layouts, :app} trong _web.ex, live_view/0 nhé.
Điều này nghĩa là tôi sẽ phải:
Kéo nội dung của app.html.heex vào module Layouts, function app
Thêm alias MiningRigMonitorWeb.Layouts trong mining_rig_monitor_web.ex, function html_heler/0
Các liveview module, ví dụ module MiningRigMonitorWeb.AsicMinerLive.Index, sau khi tôi tách function render/1 thành file index.html.heex. nếu tôi mà muốn sử dụng liveview layout có tên là app,
tôi sẽ cần phải bọc nó lại với tag <Layout.app> </Layout.app>
Tiếp theo là về cái vụ Scope. Tôi tính toán là sẽ mix phx.gen.auth ở một dự án test khác, sau đó sẽ copy quá.
Tuy nhiên, bị vướng field :authenticated_at, :utc_datetime trong Accounts.UserToken, ở phiên bản 1.7, không có field authenticated_at.
Tôi có xem kỹ hơn cái field này, có vẻ là nó liên quan đến sudo mode. Thế tính năng sudo mode là gì?
Tính năng cực kỳ thích hợp cho những tác vụ nhạy cảm. Nó sẽ yêu cầu người dùng phải đăng nhập lại trước khi đưa ra hành động nào đó.
Tôi không có nhu cầu dùng sudo mode. Cụ thể là trong dự án Mining Rig Monitor.
Thực tế, app này chỉ có 1 user role là admin. Chả có nhu cầu đụng đến Scope luôn, thôi dẹp cho khỏe.
Bài post này tổng hợp các câu hỏi tôi gặp phải khi làm việc với Phoenix Web Framework,
có những câu hỏi tôi không có câu trả lời, nhưng tôi sẽ vẫn ghi lại, khi nào rảnh tôi
sẽ quay lại nghiên cứu.
Trên con đường tập trung vào tính năng thay vì sự hoàn hảo, luôn luôn có những lúc tôi
hoàn toàn bỏ qua vẫn đề kỹ thuật mà tập trung vào nghiệp vụ nhằm khai thác tối đa
feedback loop. Bài post này sẽ giúp tôi quay lại, xử lý những vấn đề ngu ngốc mà tôi
chủ động tạo ra trong quá trình phát triển phần mềm.
001. Tại sao ở phiên bản Phoenix Web Framework cũ, phần layout chỉ cần quan tâm đến root.html.heex, nhưng bây giờ nó lại có thêm cả app.html.heex Phiên bản 1.7. Tuy nhiên ở phiên bản 1.8, nó lại bỏ đi, nhồi vào file layout.ex.
Ở thời điểm hiện tại, 21/4/2025, tôi đang có nhu cầu migrate phoenix từ 1.7 sang 1.8Official changelog. Rồi lại còn đổi từ dashboard template Flowbite Dashboard qua Nexus Dashboard của DaisyUI. Cái này mất não thật sự.
Vấn đề lớn nhất của FlowBite là giá tiền 299 USD, tiếp theo là class name. Tôi không phải là fan của việc nhìn 1 cái div có hơn 10 cái class, tôi mua Nexus Dashboard với giá 69 USD với hi vọng giải quyết vấn đề này.
root.html.heex, cái này mục đích là để render những cái html tĩnh mà thôi.
app.html.heex, cái này sử dụng kèm và đi xuyên suốt vòng đời của LiveView.
Đây là file _web.ex , phiên bản phoenix 1.7, phần live_view.
Bạn nhìn phần layout nhé, macro không hề chỉ định :app là layout mặc định của live_view. Điều này nghĩa là ở trong các live_view, cụ thể là render, phải tường
minh ghi rõ là live_view đang muốn dùng layout nào, ví dụ như:
app_admin
app_login
002. Khi làm việc với phoenix, tôi thấy có từ khóa @inner_content và @inner_block, chúng được sử dụng như thế nào.
@inner_content tìm thấy trong root.html.heex
@inner_block tìm thấy trong layout.ex, function app/1
… Vẫn còn tiếp
003. Khi khai thác conn.assigns hay socket.assigns, kỹ thuật nào có thể giúp sử dụng @tên_var thay cho Map.get(@conn.assigns, :tên_var)
Tôi chưa có câu trả lời cho câu hỏi này, tuy nhiên khi nào có thời gian tôi sẽ xem các bài sau:
004. Khi viết controller test, tôi hay thấy test không được viết trực tiếp mà được gói lại trong describe, lợi thế của nó là gì?
Lợi thế của nó là khi nhóm test này cùng cần cách setup giống nhau. Ví dụ rõ nhất là khi test update entity nào đó. Từ entity này tôi lấy từ Java Spring.
Dịch qua tiếng việt là thực thể, nhưng bạn cứ hiểu là record trong database. Khi ta muốn làm test liên quan đến update record trong database, chúng ta cần có record đó
được tạo từ trước.
Lúc này có 2 test chúng ta quan tâm:
test update với các tham số hợp lệ
test update với tham số không hợp lệ
Cả 2 test này sẽ cùng cần được tạo trước record CpuGpuMinerLog. Dưới dây là ví dụ test cho Controller của CpuGpuLog
defmoduleMiningRigMonitorWeb.CpuGpuMinerLogControllerTestdouseMiningRigMonitorWeb.ConnCaseimportMiningRigMonitor.CpuGpuMinerLogsFixturesaliasMiningRigMonitor.CpuGpuMinerLogs.CpuGpuMinerLog@update_attrs%{}@invalid_attrs%{}setup%{conn:conn}do{:ok,conn:put_req_header(conn,"accept","application/json")}enddescribe"update cpu_gpu_miner_log"dosetup[:create_cpu_gpu_miner_log]test"renders cpu_gpu_miner_log when data is valid",%{conn:conn,cpu_gpu_miner_log:%CpuGpuMinerLog{id:id}=cpu_gpu_miner_log}doconn=put(conn,~p"/api/cpu_gpu_miner_logs/#{cpu_gpu_miner_log}",cpu_gpu_miner_log:@update_attrs)assert%{"id"=>^id}=json_response(conn,200)["data"]conn=get(conn,~p"/api/cpu_gpu_miner_logs/#{id}")assert%{"id"=>^id}=json_response(conn,200)["data"]endtest"renders errors when data is invalid",%{conn:conn,cpu_gpu_miner_log:cpu_gpu_miner_log}doconn=put(conn,~p"/api/cpu_gpu_miner_logs/#{cpu_gpu_miner_log}",cpu_gpu_miner_log:@invalid_attrs)assertjson_response(conn,422)["errors"]!=%{}endenddefpcreate_cpu_gpu_miner_log(_)docpu_gpu_miner_log=cpu_gpu_miner_log_fixture()%{cpu_gpu_miner_log:cpu_gpu_miner_log}endend
Sau khi create_cpu_gpu_miner_log/1 được kích hoạt, nó trả 1 lại cái map %{}. Cái map này sẽ được nhồi tiếp vào test "xxx", %{map} do end.
Hãy chú ý dòng số 14, 16 và 27.
005. Ở trong form, tôi hay thấy :let={f}, cái này là gì thế, kỹ thuật alias từ @form thành f với let là như thế nào?
006. Luồng chạy khi tạo ``form với core_component.ex` generate mặc định là như thế nào?
007. @from[field] hoạt động như thế nào?
008. phx-update="ignore" dùng để làm gì, thấy nó sử dụng ở login form.
009. Khi chạy function trong html.heex thì cần dấu . trước tên funciton, tuy nhiên tại sao điều này lại không cần khi chạy function với module.
011. Để tiện trong việc migrate lên version Phoenix mới, tôi không muốn dụng vào core_component.ex, tôi tạo ra file mới nexus_component.ex. Trong _web.ex, sẽ xuất hiện tình trạng import 2 module , và cả 2 module đó có function tên giống nhau. Bên cạnh việc tôi có thể đổi tên function, ngoài ra, tôi có thể gài quyền ưu tiên như thế nào.
chú ý dòng 6 và 7.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defphtml_helpersdoquotedo# HTML escaping functionalityimportPhoenix.HTML# Core UI components and translationimportMiningRigMonitorWeb.CoreComponentsimportMiningRigMonitorWeb.NexusComponentsimportMiningRigMonitorWeb.Gettext# Shortcut for generating JS commandsaliasPhoenix.LiveView.JSaliasMiningRigMonitorWeb.Layouts# Routes generation with the ~p sigilunquote(verified_routes())endend
012. Nguyên lý thay đổi theme light/dark là gì
013. Tôi không hiểu cơ cấu làm việc của JS.navigate/3, JS.patch/3
Xin chào, ở bài viết này, trước tiên tôi muốn nói đến vấn đề của mình, Khi phát triển phần mềm Mining Rig Monitor, tôi muốn sử dụng
các tên tiếng Việt để đặt tên cho dàn đào. Ví dụ:
Thanh Long
Bạch Hổ
Huyền Vũ
Chu Tước
Khi gài vào phần mềm khai thác tiền mã hóa, tôi muốn những cái tên này như sau, dấu gạch ngang tôi sẽ đề cập sau:
Thanh-Long (giữ nguyên)
Bach-Ho (mất dấu nặng dưới chữ a, mũ và dấu hỏi của chữ ổ)
Huyen-Vu (chữ ề thành chữ e, chữ ũ thành chữ u )
Chu-Tuoc (ước thành uoc)
Giải pháp như sau, với function remove_diacritical_marks/1:
defremove_diacritical_marks(string)whenis_binary(string)do# á à ã ạ ả: dấu sắc, huyền, ngã, nặng, hỏilist_1=[769,768,771,803,777]# â, ă, ưlist_2=[770,774,795]string|>String.normalize(:nfd)|>String.to_charlist()|>Enum.filter(fn(e)->Enum.member?(list_1++list_2,e)==falseend)|>Kernel.to_string()end
Còn đây là test case:
defmoduleMiningRigMonitor.UtilityTestdouseExUnit.CasealiasMiningRigMonitor.Utilitytest"remove_diacritical_marks 1"dostring="""
a á à ã ạ ả
â ấ ầ ẫ ậ ẩ
ă ắ ằ ẳ ặ ẳ
e é è ẽ ẹ ẻ
ê ế ề ễ ệ ể
u ú ù ũ ụ ủ
ư ứ ừ ữ ự ử
o ó ò õ ọ ỏ
ơ ớ ờ ỡ ợ ở
"""test_result=Utility.remove_diacritical_marks(string)expected_result="""
a a a a a a
a a a a a a
a a a a a a
e e e e e e
e e e e e e
u u u u u u
u u u u u u
o o o o o o
o o o o o o
"""assert(test_result==expected_result)endtest"remove_diacritical_marks 2"dostring="""
A Á À Ã Ạ Ả
 Ấ Ầ Ẫ Ậ Ẩ
Ă Ắ Ằ Ẳ Ặ Ẳ
E É È Ẽ Ẹ Ẻ
Ê Ế Ề Ễ Ệ Ể
U Ú Ù Ũ Ụ Ủ
Ư Ứ Ừ Ữ Ự Ử
O Ó Ò Õ Ọ Ỏ
Ơ Ớ Ờ Ỡ Ợ Ở
"""test_result=Utility.remove_diacritical_marks(string)expected_result="""
A A A A A A
A A A A A A
A A A A A A
E E E E E E
E E E E E E
U U U U U U
U U U U U U
O O O O O O
O O O O O O
"""assert(test_result==expected_result)endend
Phương pháp của tôi là tách chữ có dấu thành một danh sách chữ + dấu liên quan. (Trong Elixir, module String, nó gọi là Normalization Form Canonical Decomposition - nfd).
Nguyên văn tiếng anh như sau:
:nfd - Normalization Form Canonical Decomposition. Characters are decomposed by canonical equivalence,
and multiple combining characters are arranged in a specific order.
Dưới đây là danh sách thanh sắc, ký hiệu mà tôi mò được.
# á à ã ạ ả: dấu sắc, huyền, ngã, nặng, hỏilist_1=[769,768,771,803,777]# â, ă, ưlist_2=[770,774,795]
Bạn thấy đấy, sau khi có danh sách này, việc cần làm chỉ là dùng Enum.filter/2, nếu mà char nào nằm trong nhóm list_1 & list_2 thì chúng ta loại bỏ.
Kết quả lúc này là 1 charlist []. Để biến nó thành String, tôi dùng String.to_string/1.
Còn về cái dấu gạch ngang -. Tôi sử dụng regular expression |> String.replace(~r([^a-zA-Z0-9]),"-") sau khi đã chạy qua remove_diacritical_marks/1.
Hi vọng tôi đã có thể giúp tiết kiệm 2 phút cuộc đời với cái này!