From 10182d9dbc57666dabd18fbce95b310a78bb6b71 Mon Sep 17 00:00:00 2001 From: Christian Ditaputratama <ditatompel@gmail.com> 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{}) { + <div class="max-w-sm space-y-3"> + <select + name="limit" + id="dt_limit" + class="py-2 px-3 pe-9 block bg-neutral-900 border-neutral-700 rounded-lg text-sm focus:border-orange-400 focus:ring-orange-400" + hx-get={ fmt.Sprintf("%s?%s", url, paging.EncodedQuery(q, []string{"limit"})) } + hx-trigger="change" + hx-push-url="true" + hx-target={ hxTarget } + hx-swap="outerHTML" + > + <option disabled>CHOOSE</option> + for _, page := range availablePages { + <option + value={ fmt.Sprintf("%d", page) } + selected?={ page == rowsPerPage } + >{ fmt.Sprintf("%d", page) }</option> + } + </select> + </div> +} + +templ DtRowCount(currentPage, rowsPerPage, totalRows int) { + <div> + <p class="text-sm"> + if totalRows <= 0 { + No entries found + } else { + <b>{ fmt.Sprintf("%d", (rowsPerPage * currentPage) - rowsPerPage + 1) }</b> + if rowsPerPage * currentPage > totalRows { + - <b>{ fmt.Sprintf("%d", totalRows) }</b> + } else { + - <b>{ fmt.Sprintf("%d", rowsPerPage * currentPage) }</b> + } + <b>/ { fmt.Sprintf("%d", totalRows) }</b> + } + </p> + </div> +} + +templ DtPagination(url, hxTarget string, q interface{}, p paging.Pagination) { + <div> + <nav class="pagination inline-flex gap-x-2"> + for _, page := range p.Pages { + if page == -1 { + <button class="cursor-not-allowed" disabled>...</button> + } else if page == p.CurrentPage { + <button class="active" disabled>{ fmt.Sprintf("%d", page) }</button> + } else { + <button + hx-get={ fmt.Sprintf("%s?%s&page=%d", url, paging.EncodedQuery(q, []string{"page"}), page) } + hx-push-url="true" + hx-target={ hxTarget } + hx-swap="outerHTML" + >{ fmt.Sprintf("%d", page) }</button> + } + } + </nav> + </div> +} 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("<div class=\"max-w-sm space-y-3\"><select name=\"limit\" id=\"dt_limit\" class=\"py-2 px-3 pe-9 block bg-neutral-900 border-neutral-700 rounded-lg text-sm focus:border-orange-400 focus:ring-orange-400\" hx-get=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?%s", url, paging.EncodedQuery(q, []string{"limit"}))) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 16, Col: 80} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-trigger=\"change\" hx-push-url=\"true\" hx-target=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(hxTarget) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 19, Col: 23} + } + _, 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("\" hx-swap=\"outerHTML\"><option disabled>CHOOSE</option> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, page := range availablePages { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<option value=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var4 string + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", page)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 25, Col: 36} + } + _, 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 + } + if page == rowsPerPage { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" selected") + 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", page)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 27, Col: 30} + } + _, 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("</option>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</select></div>") + 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("<div><p class=\"text-sm\">") + 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("<b>") + 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("</b> ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if rowsPerPage*currentPage > totalRows { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("- <b>") + 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("</b>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("- <b>") + 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("</b>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <b>/ ") + 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("</b>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</p></div>") + 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("<div><nav class=\"pagination inline-flex gap-x-2\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, page := range p.Pages { + if page == -1 { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"cursor-not-allowed\" disabled>...</button>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if page == p.CurrentPage { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button class=\"active\" disabled>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", page)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 58, Col: 62} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<button hx-get=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s?%s&page=%d", url, paging.EncodedQuery(q, []string{"page"}), page)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 61, Col: 96} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-push-url=\"true\" hx-target=\"") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(hxTarget) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 63, Col: 26} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\" hx-swap=\"outerHTML\">") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", page)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/handler/views/partial_datatable.templ`, Line: 65, Col: 31} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</button>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</nav></div>") + 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) { <!-- Hero --> <section class="relative overflow-hidden pt-6"> <!-- Gradients --> @@ -30,4 +37,49 @@ templ RemoteNodes() { </div> </section> <!-- End Hero --> + <div class="flex flex-col max-w-6xl mx-auto mb-10"> + <div class="min-w-full inline-block align-middle"> + @TableNodes(data, q, p) + </div> + </div> +} + +templ TableNodes(data monero.Nodes, q monero.QueryNodes, p paging.Pagination) { + <div id="tbl_nodes" class="bg-neutral-800 border border-neutral-700 rounded-xl shadow-sm overflow-hidden"> + <div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-b border-neutral-700"> + @DtRowPerPage("/remote-nodes", "#tbl_nodes", q.Limit, q) + </div> + <div class="overflow-x-auto"> + <table class="table-striped table-hover"> + <thead> + <tr> + <th scope="col">Host:Port</th> + <th scope="col">Nettype</th> + <th scope="col">Protocol</th> + <th scope="col">Country</th> + <th scope="col">Status</th> + <th scope="col">Estimate Fee</th> + <th scope="col">Uptime</th> + <th scope="col">Check</th> + </tr> + </thead> + <tbody> + for _, row := range data.Items { + <tr> + <td>{ fmt.Sprintf("%s:%d", row.Hostname, row.Port) }</td> + <td>{ row.Nettype }<br/>{ fmt.Sprintf("%d", row.Height) } </td> + <td>{ row.Protocol }</td> + <td>{ row.CountryCode }</td> + <td>{ fmt.Sprintf("%d", row.EstimateFee) }</td> + <td>{ time.Unix(row.LastChecked, 0).Format("2006-01-02 15:04") }</td> + </tr> + } + </tbody> + </table> + </div> + <div class="px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-t border-neutral-700"> + @DtRowCount(p.CurrentPage, data.RowsPerPage, data.TotalRows) + @DtPagination("/remote-nodes", "#tbl_nodes", q, p) + </div> + </div> } 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("<!-- Hero --><section class=\"relative overflow-hidden pt-6\"><!-- Gradients --><div aria-hidden=\"true\" class=\"flex absolute -top-96 start-1/2 transform -translate-x-1/2\"><div class=\"bg-gradient-to-r blur-3xl w-[25rem] h-[44rem] rotate-[-60deg] transform -translate-x-[10rem] from-amber-800/30 to-orange-800/40\"></div><div class=\"bg-gradient-to-tl blur-3xl w-[90rem] h-[50rem] rounded-fulls origin-top-left -rotate-12 -translate-x-[15rem] from-orange-900/60 via-orange-900/40 to-amber-900/80\"></div></div><!-- End Gradients --><div class=\"relative z-10\"><div class=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-10 lg:py-16\"><div class=\"text-center\"><!-- Title --><div class=\"mt-5\"><h1 class=\"block font-extrabold text-4xl md:text-5xl lg:text-6xl text-neutral-200\">Public Monero Remote Nodes List</h1></div><!-- End Title --><div class=\"mt-5\"><p class=\"text-lg text-neutral-300\"><strong>Monero remote node</strong> 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.</p></div><hr class=\"mt-6\"></div><div class=\"max-w-3xl text-center mx-auto mt-8 prose prose-invert\"><p>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.</p><p>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 <strong>Monero community suggests to <span class=\"font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500\">always run and use your own node</span></strong> to obtain the maximum possible privacy and to help decentralize the network.</p></div></div></div></section><!-- End Hero -->") + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!-- Hero --><section class=\"relative overflow-hidden pt-6\"><!-- Gradients --><div aria-hidden=\"true\" class=\"flex absolute -top-96 start-1/2 transform -translate-x-1/2\"><div class=\"bg-gradient-to-r blur-3xl w-[25rem] h-[44rem] rotate-[-60deg] transform -translate-x-[10rem] from-amber-800/30 to-orange-800/40\"></div><div class=\"bg-gradient-to-tl blur-3xl w-[90rem] h-[50rem] rounded-fulls origin-top-left -rotate-12 -translate-x-[15rem] from-orange-900/60 via-orange-900/40 to-amber-900/80\"></div></div><!-- End Gradients --><div class=\"relative z-10\"><div class=\"max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-10 lg:py-16\"><div class=\"text-center\"><!-- Title --><div class=\"mt-5\"><h1 class=\"block font-extrabold text-4xl md:text-5xl lg:text-6xl text-neutral-200\">Public Monero Remote Nodes List</h1></div><!-- End Title --><div class=\"mt-5\"><p class=\"text-lg text-neutral-300\"><strong>Monero remote node</strong> 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.</p></div><hr class=\"mt-6\"></div><div class=\"max-w-3xl text-center mx-auto mt-8 prose prose-invert\"><p>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.</p><p>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 <strong>Monero community suggests to <span class=\"font-extrabold text-2xl underline decoration-double decoration-2 decoration-pink-500\">always run and use your own node</span></strong> to obtain the maximum possible privacy and to help decentralize the network.</p></div></div></div></section><!-- End Hero --><div class=\"flex flex-col max-w-6xl mx-auto mb-10\"><div class=\"min-w-full inline-block align-middle\">") + 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("</div></div>") + 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("<div id=\"tbl_nodes\" class=\"bg-neutral-800 border border-neutral-700 rounded-xl shadow-sm overflow-hidden\"><div class=\"px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-b border-neutral-700\">") + 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("</div><div class=\"overflow-x-auto\"><table class=\"table-striped table-hover\"><thead><tr><th scope=\"col\">Host:Port</th><th scope=\"col\">Nettype</th><th scope=\"col\">Protocol</th><th scope=\"col\">Country</th><th scope=\"col\">Status</th><th scope=\"col\">Estimate Fee</th><th scope=\"col\">Uptime</th><th scope=\"col\">Check</th></tr></thead> <tbody>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, row := range data.Items { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<tr><td>") + 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("</td><td>") + 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("<br>") + 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("</td><td>") + 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("</td><td>") + 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("</td><td>") + 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("</td><td>") + 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("</td></tr>") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</tbody></table></div><div class=\"px-6 py-4 grid gap-3 md:flex md:justify-between md:items-center border-t border-neutral-700\">") + 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("</div></div>") 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()