Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2989fa224d |
@@ -628,6 +628,10 @@ func (s *Server) setMode(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var nu *model.NotUpdatableError
|
||||
if err := s.jobs.AutoAllowed(svc); errors.As(err, &nu) {
|
||||
if isHTMX(r) {
|
||||
// htmx does not swap a 409; the service page shows the reason.
|
||||
w.Header().Set("HX-Redirect", "/services/"+strconv.FormatInt(id, 10))
|
||||
}
|
||||
s.renderServiceError(w, r, id, http.StatusConflict, "Cannot enable auto: "+nu.Reason+".")
|
||||
return
|
||||
} else if err != nil {
|
||||
@@ -641,6 +645,11 @@ func (s *Server) setMode(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
s.broker.Publish(events.Services)
|
||||
if r.PostFormValue("return") == "overview" {
|
||||
if isHTMX(r) {
|
||||
// Swap the list in place instead of redirecting, so the page keeps its scroll position.
|
||||
s.servicesFragment(w, r)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
@@ -881,6 +890,11 @@ func (s *Server) fail(w http.ResponseWriter, err error) {
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// isHTMX reports whether r was sent by htmx rather than by a plain form submit.
|
||||
func isHTMX(r *http.Request) bool {
|
||||
return r.Header.Get("HX-Request") == "true"
|
||||
}
|
||||
|
||||
// agentSnippet is the compose file shown once when an agent is added.
|
||||
func agentSnippet(cfg Config, token string) string {
|
||||
return fmt.Sprintf(`services:
|
||||
|
||||
@@ -707,6 +707,47 @@ func TestSetMode_ReturnToOverview(t *testing.T) {
|
||||
assert.Equal(t, "auto", store.modeOf(1))
|
||||
}
|
||||
|
||||
func postModeHTMX(t *testing.T, srv *httptest.Server, id string, form url.Values) (*http.Response, string) {
|
||||
t.Helper()
|
||||
form.Set("csrf_token", "csrf")
|
||||
req, err := http.NewRequest(http.MethodPost, srv.URL+"/services/"+id+"/mode", strings.NewReader(form.Encode()))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("HX-Request", "true")
|
||||
resp, err := noRedirectClient().Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return resp, string(body)
|
||||
}
|
||||
|
||||
func TestSetMode_HTMXFromOverviewReturnsServicesFragment(t *testing.T) {
|
||||
srv, store, _, broker, _ := newTestEnv(t)
|
||||
sub, cancel := broker.Subscribe()
|
||||
defer cancel()
|
||||
|
||||
resp, body := postModeHTMX(t, srv, "1", url.Values{"mode": {"auto"}, "return": {"overview"}})
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode, "no redirect, so the page keeps its scroll position")
|
||||
assert.Empty(t, resp.Header.Get("Location"))
|
||||
assert.NotContains(t, body, "<html")
|
||||
assert.Contains(t, body, `id="machines" hx-swap-oob`)
|
||||
assert.Contains(t, body, `action="/services/1/mode"`)
|
||||
assert.Equal(t, "auto", store.modeOf(1))
|
||||
assert.Equal(t, events.Services, <-sub)
|
||||
}
|
||||
|
||||
func TestSetMode_HTMXRefusedRedirectsToService(t *testing.T) {
|
||||
srv, store, _, _, jobs := newTestEnv(t)
|
||||
jobs.autoErr = &model.NotUpdatableError{Reason: model.ReasonBuilt}
|
||||
|
||||
resp, _ := postModeHTMX(t, srv, "1", url.Values{"mode": {"auto"}, "return": {"overview"}})
|
||||
|
||||
assert.Equal(t, http.StatusConflict, resp.StatusCode)
|
||||
assert.Equal(t, "/services/1", resp.Header.Get("HX-Redirect"), "htmx does not swap a 409, so send the user to the page that shows why")
|
||||
assert.Equal(t, "manual", store.modeOf(1))
|
||||
}
|
||||
|
||||
func TestSetMode_AutoRefusedShows409(t *testing.T) {
|
||||
srv, store, _, _, jobs := newTestEnv(t)
|
||||
jobs.autoErr = &model.NotUpdatableError{Reason: model.ReasonBuilt}
|
||||
|
||||
@@ -66,6 +66,9 @@ summary.btn::-webkit-details-marker{display:none}
|
||||
.seg button{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 10px;border:0;border-radius:5px;background:transparent;color:var(--mut);font:500 12.5px var(--sans);cursor:pointer}
|
||||
.seg button:hover{color:var(--fg)}
|
||||
.seg button[aria-pressed=true]{background:var(--hover);color:var(--fg);box-shadow:inset 0 0 0 1px var(--line2)}
|
||||
/* htmx inserts the new list before removing the old one; scroll anchoring on the
|
||||
focused switch would scroll by the list height and then land at the bottom. */
|
||||
#services{overflow-anchor:none}
|
||||
.grp{margin-top:28px}
|
||||
.gh{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 12px;padding-bottom:8px;border-bottom:2px solid var(--fg)}
|
||||
.gh h2{margin:0;font:700 20px var(--sans);letter-spacing:-.015em}
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
{{if .Self}}<span class="tag f-out">This agent</span>{{end}}
|
||||
</div>
|
||||
<div class="mode">
|
||||
<form method="post" action="/services/{{.ID}}/mode">
|
||||
<form method="post" action="/services/{{.ID}}/mode" hx-post="/services/{{.ID}}/mode" hx-target="#services" hx-swap="innerHTML">
|
||||
<input type="hidden" name="csrf_token" value="{{$.CSRF}}">
|
||||
<input type="hidden" name="return" value="overview">
|
||||
{{if eq .UpdateMode "auto"}}<input type="hidden" name="mode" value="manual">
|
||||
|
||||
Reference in New Issue
Block a user