Skip to content

Commit

Permalink
Loading indicator for delete spam samples (#69)
Browse files Browse the repository at this point in the history
* add loading indicator on delete #61

* fix tests
  • Loading branch information
umputun committed Mar 25, 2024
1 parent 9a634b0 commit ef2340b
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 56 deletions.
24 changes: 12 additions & 12 deletions app/webapi/assets/components/samples_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ <h4>
</h4>
<ul class="list-group" id="spam-samples-list">
{{range .SpamSamples}}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{.}}
<form method="POST" hx-post="/delete/spam" hx-target="#samples-list" hx-swap="outerHTML">
<input type="hidden" name="msg" value="{{.}}">
<button type="submit" class="btn btn-sm btn-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span id="loading-{{.ID}}">{{.Sample}} <img class="htmx-indicator" src="/spinner.svg"/></span>
<form method="POST" hx-post="/delete/spam" hx-target="#samples-list" hx-swap="outerHTML" hx-indicator="#loading-{{.ID}}">
<input type="hidden" name="msg" value="{{.Sample}}">
<button type="submit" class="btn btn-sm btn-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</li>
{{else}}
<li class="list-group-item">No spam samples found</li>
<li class="list-group-item">No spam samples found</li>
{{end}}
</ul>
</div>
Expand All @@ -29,9 +29,9 @@ <h4>
<ul class="list-group" id="ham-samples-list">
{{range .HamSamples}}
<li class="list-group-item d-flex justify-content-between align-items-center">
{{.}}
{{.Sample}}
<form method="POST" hx-post="/delete/ham" hx-target="#samples-list" hx-swap="outerHTML">
<input type="hidden" name="msg" value="{{.}}">
<input type="hidden" name="msg" value="{{.Sample}}">
<button type="submit" class="btn btn-sm btn-danger">
<i class="bi bi-trash"></i>
</button>
Expand Down
1 change: 1 addition & 0 deletions app/webapi/assets/spinner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions app/webapi/assets/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,15 @@ body, #result{
color: #fff;
background-color: #3286cb;
border-color: #3d86e5;
}

.htmx-indicator{
opacity:0;
transition: opacity 500ms ease-in;
}
.htmx-request .htmx-indicator{
opacity:1
}
.htmx-request.htmx-indicator{
opacity:1
}
83 changes: 41 additions & 42 deletions app/webapi/webapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package webapi
import (
"context"
"crypto/rand"
"crypto/sha1" //nolint
"embed"
"encoding/json"
"errors"
Expand Down Expand Up @@ -199,7 +200,8 @@ func (s *Server) routes(router *chi.Mux) *chi.Mux {
webUI.Get("/detected_spam", s.htmlDetectedSpamHandler) // serve detected spam page
webUI.Get("/list_settings", s.htmlSettingsHandler) // serve settings
webUI.Get("/styles.css", s.stylesHandler) // serve styles.css
webUI.Get("/logo.png", s.logoutHandler) // serve logo.png
webUI.Get("/logo.png", s.logoHandler) // serve logo.png
webUI.Get("/spinner.svg", s.spinnerHandler) // serve spinner.svg
webUI.Post("/detected_spam/add", s.htmlAddDetectedSpamHandler) // add detected spam to samples
})

Expand Down Expand Up @@ -321,7 +323,7 @@ func (s *Server) updateSampleHandler(updFn func(msg string) error) func(w http.R
}

if isHtmxRequest {
s.renderSamples(w)
s.renderSamples(w, "samples_list.html")
} else {
rest.RenderJSON(w, rest.JSON{"updated": true, "msg": req.Msg})
}
Expand Down Expand Up @@ -353,7 +355,7 @@ func (s *Server) deleteSampleHandler(delFn func(msg string) (int, error)) func(w
}

if isHtmxRequest {
s.renderSamples(w)
s.renderSamples(w, "samples_list.html")
} else {
rest.RenderJSON(w, rest.JSON{"deleted": true, "msg": req.Msg, "count": count})
}
Expand Down Expand Up @@ -460,32 +462,7 @@ func (s *Server) htmlSpamCheckHandler(w http.ResponseWriter, _ *http.Request) {
// htmlManageSamplesHandler handles GET /manage_samples request.
// It returns rendered manage_samples.html template with all the components.
func (s *Server) htmlManageSamplesHandler(w http.ResponseWriter, _ *http.Request) {
spam, ham, err := s.SpamFilter.DynamicSamples()
if err != nil {
log.Printf("[ERROR] Failed to fetch dynamic samples: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
spam, ham = s.reverseSamples(spam, ham)

tmplData := struct {
SpamSamples []string
HamSamples []string
TotalSpamSamples int
TotalHamSamples int
}{
SpamSamples: spam,
HamSamples: ham,
TotalSpamSamples: len(spam),
TotalHamSamples: len(ham),
}

// Execute the manage_samples template with the data
if err := tmpl.ExecuteTemplate(w, "manage_samples.html", tmplData); err != nil {
log.Printf("[WARN] failed to execute template: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
s.renderSamples(w, "manage_samples.html")
}

func (s *Server) htmlManageUsersHandler(w http.ResponseWriter, _ *http.Request) {
Expand Down Expand Up @@ -586,8 +563,8 @@ func (s *Server) stylesHandler(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write(body)
}

// logoutHandler handles GET /logo.png request. It returns assets/logo.png file.
func (s *Server) logoutHandler(w http.ResponseWriter, _ *http.Request) {
// logoHandler handles GET /logo.png request. It returns assets/logo.png file.
func (s *Server) logoHandler(w http.ResponseWriter, _ *http.Request) {
img, err := templateFS.ReadFile("assets/logo.png")
if err != nil {
http.Error(w, "Logo not found", http.StatusNotFound)
Expand All @@ -598,35 +575,57 @@ func (s *Server) logoutHandler(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write(img)
}

func (s *Server) renderSamples(w http.ResponseWriter) {
spam, ham, err := s.SpamFilter.DynamicSamples()
func (s *Server) spinnerHandler(w http.ResponseWriter, _ *http.Request) {
img, err := templateFS.ReadFile("assets/spinner.svg")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
rest.RenderJSON(w, rest.JSON{"error": "can't fetch samples", "details": err.Error()})
http.Error(w, "Logo not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "image/svg+xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(img)
}

tmpl, err := template.New("").ParseFS(templateFS, "assets/components/samples_list.html")
func (s *Server) renderSamples(w http.ResponseWriter, tmplName string) {
spam, ham, err := s.SpamFilter.DynamicSamples()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
rest.RenderJSON(w, rest.JSON{"error": "can't parse template", "details": err.Error()})
rest.RenderJSON(w, rest.JSON{"error": "can't fetch samples", "details": err.Error()})
return
}

spam, ham = s.reverseSamples(spam, ham)

type smpleWithID struct {
ID string
Sample string
}

makeID := func(s string) string {
hash := sha1.New() //nolint
if _, err := hash.Write([]byte(s)); err != nil {
return fmt.Sprintf("%x", s)
}
return fmt.Sprintf("%x", hash.Sum(nil))
}

tmplData := struct {
SpamSamples []string
HamSamples []string
SpamSamples []smpleWithID
HamSamples []smpleWithID
TotalHamSamples int
TotalSpamSamples int
}{
SpamSamples: spam,
HamSamples: ham,
TotalHamSamples: len(ham),
TotalSpamSamples: len(spam),
}
for _, s := range spam {
tmplData.SpamSamples = append(tmplData.SpamSamples, smpleWithID{ID: makeID(s), Sample: s})
}
for _, h := range ham {
tmplData.HamSamples = append(tmplData.HamSamples, smpleWithID{ID: makeID(h), Sample: h})
}

if err := tmpl.ExecuteTemplate(w, "samples_list.html", tmplData); err != nil {
if err := tmpl.ExecuteTemplate(w, tmplName, tmplData); err != nil {
w.WriteHeader(http.StatusInternalServerError)
rest.RenderJSON(w, rest.JSON{"error": "can't execute template", "details": err.Error()})
return
Expand Down
4 changes: 2 additions & 2 deletions app/webapi/webapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,7 @@ func TestServer_logoHandler(t *testing.T) {
req, err := http.NewRequest("GET", "/logo.png", http.NoBody)
require.NoError(t, err)

handler := http.HandlerFunc(server.logoutHandler)
handler := http.HandlerFunc(server.logoHandler)
handler.ServeHTTP(rr, req)

assert.Equal(t, http.StatusOK, rr.Code, "handler should return status OK")
Expand Down Expand Up @@ -1111,7 +1111,7 @@ func TestServer_renderSamples(t *testing.T) {
SpamFilter: mockSpamFilter,
})
w := httptest.NewRecorder()
server.renderSamples(w)
server.renderSamples(w, "samples_list.html")
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
t.Log(w.Body.String())
Expand Down

0 comments on commit ef2340b

Please sign in to comment.