From 10182d9dbc57666dabd18fbce95b310a78bb6b71 Mon Sep 17 00:00:00 2001 From: Christian Ditaputratama Date: Thu, 31 Oct 2024 22:45:26 +0700 Subject: [PATCH] feat!: Added base datatable functionality Deprecated: `SortDirection` is deprecated, use `SortDir` instead --- internal/handler/response.go | 99 ++++-- .../handler/views/partial_datatable.templ | 70 ++++ .../handler/views/partial_datatable_templ.go | 333 ++++++++++++++++++ internal/handler/views/remote_nodes.templ | 54 ++- internal/handler/views/remote_nodes_templ.go | 169 ++++++++- internal/handler/views/src/css/main.css | 8 + internal/monero/monero.go | 29 +- internal/monero/monero_test.go | 102 +++--- 8 files changed, 774 insertions(+), 90 deletions(-) create mode 100644 internal/handler/views/partial_datatable.templ create mode 100644 internal/handler/views/partial_datatable_templ.go diff --git a/internal/handler/response.go b/internal/handler/response.go index cfa506f..6024a93 100644 --- a/internal/handler/response.go +++ b/internal/handler/response.go @@ -7,6 +7,7 @@ import ( "github.com/a-h/templ" "github.com/ditatompel/xmr-remote-nodes/internal/handler/views" "github.com/ditatompel/xmr-remote-nodes/internal/monero" + "github.com/ditatompel/xmr-remote-nodes/internal/paging" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/adaptor" @@ -30,24 +31,6 @@ func (s *fiberServer) homeHandler(c *fiber.Ctx) error { return handler(c) } -// Render Remote Nodes Page -func (s *fiberServer) remoteNodesHandler(c *fiber.Ctx) error { - p := views.Meta{ - Title: "Public Monero Remote Nodes List", - Description: "Although it's possible to use these existing public Monero nodes, you're MUST RUN AND USE YOUR OWN NODE!", - Keywords: "monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc", - Robots: "INDEX,FOLLOW", - Permalink: "https://xmr.ditatompel.com/remote-nodes", - Identifier: "/remote-nodes", - } - - c.Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, p.Permalink)) - home := views.BaseLayout(p, views.RemoteNodes()) - handler := adaptor.HTTPHandler(templ.Handler(home)) - - return handler(c) -} - // Render Add Node Page func (s *fiberServer) addNodeHandler(c *fiber.Ctx) error { p := views.Meta{ @@ -102,20 +85,78 @@ func Node(c *fiber.Ctx) error { }) } -// Returns a list of nodes +// Render Remote Nodes Page +func (s *fiberServer) remoteNodesHandler(c *fiber.Ctx) error { + p := views.Meta{ + Title: "Public Monero Remote Nodes List", + Description: "Although it's possible to use these existing public Monero nodes, you're MUST RUN AND USE YOUR OWN NODE!", + Keywords: "monero remote nodes,public monero nodes,monero public nodes,monero wallet,tor monero node,monero cors rpc", + Robots: "INDEX,FOLLOW", + Permalink: "https://xmr.ditatompel.com/remote-nodes", + Identifier: "/remote-nodes", + } + + moneroRepo := monero.New() + query := monero.QueryNodes{ + Paging: paging.Paging{ + Limit: c.QueryInt("limit", 10), // rows per page + Page: c.QueryInt("page", 1), + SortBy: c.Query("sort_by", "id"), + SortDir: c.Query("sort_dir", "desc"), + SortDirection: c.Query("sort_direction", "desc"), // deprecated + Refresh: c.QueryInt("refresh", 0), + }, + Host: c.Query("host"), + Nettype: c.Query("nettype", "any"), + Protocol: c.Query("protocol", "any"), + CC: c.Query("cc", "any"), + Status: c.QueryInt("status", -1), + CORS: c.QueryInt("cors", -1), + } + + nodes, err := moneroRepo.Nodes(query) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "status": "error", + "message": err.Error(), + "data": nil, + }) + } + + pagination := paging.NewPagination(query.Page, nodes.TotalPages) + + // handle request from HTMX + if c.Get("HX-Target") == "tbl_nodes" { + cmp := views.BlankLayout(views.TableNodes(nodes, query, pagination)) + handler := adaptor.HTTPHandler(templ.Handler(cmp)) + return handler(c) + } + + c.Set("Link", fmt.Sprintf(`<%s>; rel="canonical"`, p.Permalink)) + home := views.BaseLayout(p, views.RemoteNodes(nodes, query, pagination)) + handler := adaptor.HTTPHandler(templ.Handler(home)) + + return handler(c) +} + +// Returns a list of nodes (API) func Nodes(c *fiber.Ctx) error { moneroRepo := monero.New() query := monero.QueryNodes{ - RowsPerPage: c.QueryInt("limit", 10), - Page: c.QueryInt("page", 1), - SortBy: c.Query("sort_by", "id"), - SortDirection: c.Query("sort_direction", "desc"), - Host: c.Query("host"), - Nettype: c.Query("nettype", "any"), - Protocol: c.Query("protocol", "any"), - CC: c.Query("cc", "any"), - Status: c.QueryInt("status", -1), - CORS: c.QueryInt("cors", -1), + Paging: paging.Paging{ + Limit: c.QueryInt("limit", 10), // rows per page + Page: c.QueryInt("page", 1), + SortBy: c.Query("sort_by", "id"), + SortDir: c.Query("sort_dir", "desc"), + SortDirection: c.Query("sort_direction", "desc"), // deprecated + Refresh: c.QueryInt("refresh", 0), + }, + Host: c.Query("host"), + Nettype: c.Query("nettype", "any"), + Protocol: c.Query("protocol", "any"), + CC: c.Query("cc", "any"), + Status: c.QueryInt("status", -1), + CORS: c.QueryInt("cors", -1), } nodes, err := moneroRepo.Nodes(query) diff --git a/internal/handler/views/partial_datatable.templ b/internal/handler/views/partial_datatable.templ new file mode 100644 index 0000000..b07e93f --- /dev/null +++ b/internal/handler/views/partial_datatable.templ @@ -0,0 +1,70 @@ +package views + +import ( + "fmt" + "github.com/ditatompel/xmr-remote-nodes/internal/paging" +) + +var availablePages = []int{5, 10, 20, 50, 100} + +templ DtRowPerPage(url, hxTarget string, rowsPerPage int, q interface{}) { +
+ +
+} + +templ DtRowCount(currentPage, rowsPerPage, totalRows int) { +
+

+ if totalRows <= 0 { + No entries found + } else { + { fmt.Sprintf("%d", (rowsPerPage * currentPage) - rowsPerPage + 1) } + if rowsPerPage * currentPage > totalRows { + - { fmt.Sprintf("%d", totalRows) } + } else { + - { fmt.Sprintf("%d", rowsPerPage * currentPage) } + } + / { fmt.Sprintf("%d", totalRows) } + } +

+
+} + +templ DtPagination(url, hxTarget string, q interface{}, p paging.Pagination) { +
+ +
+} diff --git a/internal/handler/views/partial_datatable_templ.go b/internal/handler/views/partial_datatable_templ.go new file mode 100644 index 0000000..2cd1341 --- /dev/null +++ b/internal/handler/views/partial_datatable_templ.go @@ -0,0 +1,333 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.2.778 +package views + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import templruntime "github.com/a-h/templ/runtime" + +import ( + "fmt" + "github.com/ditatompel/xmr-remote-nodes/internal/paging" +) + +var availablePages = []int{5, 10, 20, 50, 100} + +func DtRowPerPage(url, hxTarget string, rowsPerPage int, q interface{}) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +func DtRowCount(currentPage, rowsPerPage, totalRows int) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if totalRows <= 0 { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("No entries found") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", (rowsPerPage*currentPage)-rowsPerPage+1)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 39, Col: 73} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if rowsPerPage*currentPage > totalRows { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("- ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalRows)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 41, Col: 40} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("- ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", rowsPerPage*currentPage)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 43, Col: 56} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" / ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalRows)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 45, Col: 39} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +func DtPagination(url, hxTarget string, q interface{}, p paging.Pagination) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var11 := templ.GetChildren(ctx) + if templ_7745c5c3_Var11 == nil { + templ_7745c5c3_Var11 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/internal/handler/views/remote_nodes.templ b/internal/handler/views/remote_nodes.templ index 943f3c4..6225f9d 100644 --- a/internal/handler/views/remote_nodes.templ +++ b/internal/handler/views/remote_nodes.templ @@ -1,6 +1,13 @@ package views -templ RemoteNodes() { +import ( + "fmt" + "github.com/ditatompel/xmr-remote-nodes/internal/monero" + "github.com/ditatompel/xmr-remote-nodes/internal/paging" + "time" +) + +templ RemoteNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) {
@@ -30,4 +37,49 @@ templ RemoteNodes() {
+
+
+ @TableNodes(data, q, p) +
+
+} + +templ TableNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) { +
+
+ @DtRowPerPage("/remote-nodes", "#tbl_nodes", q.Limit, q) +
+
+ + + + + + + + + + + + + + + for _, row := range data.Items { + + + + + + + + + } + +
Host:PortNettypeProtocolCountryStatusEstimate FeeUptimeCheck
{ fmt.Sprintf("%s:%d", row.Hostname, row.Port) }{ row.Nettype }
{ fmt.Sprintf("%d", row.Height) }
{ row.Protocol }{ row.CountryCode }{ fmt.Sprintf("%d", row.EstimateFee) }{ time.Unix(row.LastChecked, 0).Format("2006-01-02 15:04") }
+
+
+ @DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows) + @DtPagination("/remote-nodes", "#tbl_nodes", q, p) +
+
} diff --git a/internal/handler/views/remote_nodes_templ.go b/internal/handler/views/remote_nodes_templ.go index a70436c..3698a26 100644 --- a/internal/handler/views/remote_nodes_templ.go +++ b/internal/handler/views/remote_nodes_templ.go @@ -8,7 +8,14 @@ package views import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -func RemoteNodes() templ.Component { +import ( + "fmt" + "github.com/ditatompel/xmr-remote-nodes/internal/monero" + "github.com/ditatompel/xmr-remote-nodes/internal/paging" + "time" +) + +func RemoteNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -29,7 +36,165 @@ func RemoteNodes() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Public Monero Remote Nodes List

Monero remote node is a device on the internet running the Monero software with full copy of the Monero blockchain that doesn't run on the same local machine where the Monero wallet is located.


Remote node can be used by people who, for their own reasons (usually because of hardware requirements, disk space, or technical abilities), cannot/don't want to run their own node and prefer to relay on one publicly available on the Monero network.

Using an open node will allow to make a transaction instantaneously, without the need to download the blockchain and sync to the Monero network first, but at the cost of the control over your privacy. the Monero community suggests to always run and use your own node to obtain the maximum possible privacy and to help decentralize the network.

") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Public Monero Remote Nodes List

Monero remote node is a device on the internet running the Monero software with full copy of the Monero blockchain that doesn't run on the same local machine where the Monero wallet is located.


Remote node can be used by people who, for their own reasons (usually because of hardware requirements, disk space, or technical abilities), cannot/don't want to run their own node and prefer to relay on one publicly available on the Monero network.

Using an open node will allow to make a transaction instantaneously, without the need to download the blockchain and sync to the Monero network first, but at the cost of the control over your privacy. the Monero community suggests to always run and use your own node to obtain the maximum possible privacy and to help decentralize the network.

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = TableNodes(data, q, p).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return templ_7745c5c3_Err + }) +} + +func TableNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var2 := templ.GetChildren(ctx) + if templ_7745c5c3_Var2 == nil { + templ_7745c5c3_Var2 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = DtRowPerPage("/remote-nodes", "#tbl_nodes", q.Limit, q).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, row := range data.Items { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
Host:PortNettypeProtocolCountryStatusEstimate FeeUptimeCheck
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s:%d", row.Hostname, row.Port)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 69, Col: 57} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(row.Nettype) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 70, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", row.Height)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 70, Col: 62} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(row.Protocol) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 71, Col: 25} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 string + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(row.CountryCode) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 72, Col: 28} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", row.EstimateFee)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 73, Col: 47} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(time.Unix(row.LastChecked, 0).Format("2006-01-02 15:04")) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/remote_nodes.templ`, Line: 74, Col: 69} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = DtPagination("/remote-nodes", "#tbl_nodes", q, p).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/internal/handler/views/src/css/main.css b/internal/handler/views/src/css/main.css index 6aed813..d8681a6 100644 --- a/internal/handler/views/src/css/main.css +++ b/internal/handler/views/src/css/main.css @@ -25,3 +25,11 @@ a.btn-link { button.copy-input { @apply px-2 shrink-0 inline-flex justify-center items-center gap-x-2 text-sm font-semibold rounded-e-md border border-transparent bg-orange-600 text-white hover:brightness-125 focus:outline-none focus:bg-orange-700 disabled:opacity-50 disabled:pointer-events-none; } + +/* pagination */ +nav.pagination button.active { + @apply py-1.5 px-2 inline-flex items-center gap-x-2 text-sm font-bold rounded-lg border border-orange-500 bg-orange-500 text-white shadow-sm hover:brightness-125 disabled:opacity-90 disabled:pointer-events-none; +} +nav.pagination button { + @apply py-1.5 px-2 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg bg-neutral-800 border border-neutral-700 text-white shadow-sm hover:brightness-125 disabled:opacity-50 disabled:pointer-events-none; +} diff --git a/internal/monero/monero.go b/internal/monero/monero.go index daec765..9f69e99 100644 --- a/internal/monero/monero.go +++ b/internal/monero/monero.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log/slog" + "math" "net" "slices" "strings" @@ -13,6 +14,7 @@ import ( "github.com/ditatompel/xmr-remote-nodes/internal/database" "github.com/ditatompel/xmr-remote-nodes/internal/ip" + "github.com/ditatompel/xmr-remote-nodes/internal/paging" "github.com/jmoiron/sqlx/types" ) @@ -74,18 +76,13 @@ func (r *moneroRepo) Node(id int) (Node, error) { // QueryNodes represents database query parameters type QueryNodes struct { - Host string + paging.Paging + Host string `url:"host,omitempty"` Nettype string // Can be "any", mainnet, stagenet, testnet. Default: "any" Protocol string // Can be "any", tor, http, https. Default: "any" - CC string // 2 letter country code + CC string `url:"cc,omitempty"` // 2 letter country code Status int CORS int - - // pagination - RowsPerPage int - Page int - SortBy string - SortDirection string } // toSQL generates SQL query from query parameters @@ -133,8 +130,14 @@ func (q *QueryNodes) toSQL() (args []interface{}, where string) { if !slices.Contains([]string{"last_checked", "uptime"}, q.SortBy) { q.SortBy = "last_checked" } + + // deprecated: Use SortDir instead if q.SortDirection != "asc" { - q.SortDirection = "DESC" + q.SortDir = "DESC" + } + + if q.SortDir != "asc" { + q.SortDir = "DESC" } return args, where @@ -143,6 +146,7 @@ func (q *QueryNodes) toSQL() (args []interface{}, where string) { // Nodes represents a list of nodes type Nodes struct { TotalRows int `json:"total_rows"` + TotalPages int `json:"total_pages"` // total pages RowsPerPage int `json:"rows_per_page"` Items []*Node `json:"items"` } @@ -153,7 +157,7 @@ func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) { var nodes Nodes - nodes.RowsPerPage = q.RowsPerPage + nodes.RowsPerPage = q.Limit qTotal := fmt.Sprintf(` SELECT @@ -166,7 +170,8 @@ func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) { if err != nil { return nodes, err } - args = append(args, q.RowsPerPage, (q.Page-1)*q.RowsPerPage) + nodes.TotalPages = int(math.Ceil(float64(nodes.TotalRows) / float64(q.Limit))) + args = append(args, q.Limit, (q.Page-1)*q.Limit) query := fmt.Sprintf(` SELECT @@ -178,7 +183,7 @@ func (r *moneroRepo) Nodes(q QueryNodes) (Nodes, error) { %s %s LIMIT ? - OFFSET ?`, where, q.SortBy, q.SortDirection) + OFFSET ?`, where, q.SortBy, q.SortDir) err = r.db.Select(&nodes.Items, query, args...) return nodes, err diff --git a/internal/monero/monero_test.go b/internal/monero/monero_test.go index b9cddf5..ad0625c 100644 --- a/internal/monero/monero_test.go +++ b/internal/monero/monero_test.go @@ -8,6 +8,7 @@ import ( "github.com/ditatompel/xmr-remote-nodes/internal/config" "github.com/ditatompel/xmr-remote-nodes/internal/database" + "github.com/ditatompel/xmr-remote-nodes/internal/paging" ) var testMySQL = true @@ -39,50 +40,56 @@ func init() { // go test -race ./internal/monero -run=TestQueryNodes_toSQL -v func TestQueryNodes_toSQL(t *testing.T) { tests := []struct { - name string - query QueryNodes - wantArgs []interface{} - wantWhere string - wantSortBy string - wantSortDirection string + name string + query QueryNodes + wantArgs []interface{} + wantWhere string + wantSortBy string + wantSortDir string }{ { name: "Default query", query: QueryNodes{ - Host: "", - Nettype: "any", - Protocol: "any", - CC: "any", - Status: -1, - CORS: -1, - RowsPerPage: 10, - Page: 1, - SortBy: "last_checked", - SortDirection: "desc", + Paging: paging.Paging{ + Limit: 10, + Page: 1, + SortBy: "last_checked", + SortDir: "desc", + SortDirection: "desc", // deprecated + }, + Host: "", + Nettype: "any", + Protocol: "any", + CC: "any", + Status: -1, + CORS: -1, }, - wantArgs: []interface{}{}, - wantWhere: "", - wantSortBy: "last_checked", - wantSortDirection: "DESC", + wantArgs: []interface{}{}, + wantWhere: "", + wantSortBy: "last_checked", + wantSortDir: "DESC", }, { name: "With host query", query: QueryNodes{ - Host: "test", - Nettype: "any", - Protocol: "any", - CC: "any", - Status: -1, - CORS: -1, - RowsPerPage: 10, - Page: 1, - SortBy: "last_checked", - SortDirection: "desc", + Paging: paging.Paging{ + Limit: 10, + Page: 1, + SortBy: "last_checked", + SortDir: "desc", + SortDirection: "desc", // deprecated + }, + Host: "test", + Nettype: "any", + Protocol: "any", + CC: "any", + Status: -1, + CORS: -1, }, - wantArgs: []interface{}{"%test%", "%test%"}, - wantWhere: "WHERE (hostname LIKE ? OR ip_addr LIKE ?)", - wantSortBy: "last_checked", - wantSortDirection: "DESC", + wantArgs: []interface{}{"%test%", "%test%"}, + wantWhere: "WHERE (hostname LIKE ? OR ip_addr LIKE ?)", + wantSortBy: "last_checked", + wantSortDir: "DESC", }, } for _, tt := range tests { @@ -97,8 +104,8 @@ func TestQueryNodes_toSQL(t *testing.T) { if tt.query.SortBy != tt.wantSortBy { t.Errorf("QueryNodes.toSQL() gotSortBy = %v, want %v", tt.query.SortBy, tt.wantSortBy) } - if tt.query.SortDirection != tt.wantSortDirection { - t.Errorf("QueryNodes.toSQL() gotSortDirection = %v, want %v", tt.query.SortDirection, tt.wantSortDirection) + if tt.query.SortDir != tt.wantSortDir { + t.Errorf("QueryNodes.toSQL() gotSortDir = %v, want %v", tt.query.SortDir, tt.wantSortDir) } }) } @@ -108,16 +115,19 @@ func TestQueryNodes_toSQL(t *testing.T) { // go test ./internal/monero -bench QueryNodes_toSQL -benchmem -run=^$ -v func Benchmark_QueryNodes_toSQL(b *testing.B) { q := QueryNodes{ - Host: "test", - Nettype: "any", - Protocol: "any", - CC: "any", - Status: -1, - CORS: -1, - RowsPerPage: 10, - Page: 1, - SortBy: "last_checked", - SortDirection: "desc", + Paging: paging.Paging{ + Limit: 10, + Page: 1, + SortBy: "last_checked", + SortDir: "desc", + SortDirection: "desc", // deprecated + }, + Host: "test", + Nettype: "any", + Protocol: "any", + CC: "any", + Status: -1, + CORS: -1, } for i := 0; i < b.N; i++ { _, _ = q.toSQL()