Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2989fa224d |
@@ -628,6 +628,10 @@ func (s *Server) setMode(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
var nu *model.NotUpdatableError
|
var nu *model.NotUpdatableError
|
||||||
if err := s.jobs.AutoAllowed(svc); errors.As(err, &nu) {
|
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+".")
|
s.renderServiceError(w, r, id, http.StatusConflict, "Cannot enable auto: "+nu.Reason+".")
|
||||||
return
|
return
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@@ -641,6 +645,11 @@ func (s *Server) setMode(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
s.broker.Publish(events.Services)
|
s.broker.Publish(events.Services)
|
||||||
if r.PostFormValue("return") == "overview" {
|
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)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -881,6 +890,11 @@ func (s *Server) fail(w http.ResponseWriter, err error) {
|
|||||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
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.
|
// agentSnippet is the compose file shown once when an agent is added.
|
||||||
func agentSnippet(cfg Config, token string) string {
|
func agentSnippet(cfg Config, token string) string {
|
||||||
return fmt.Sprintf(`services:
|
return fmt.Sprintf(`services:
|
||||||
|
|||||||
@@ -707,6 +707,47 @@ func TestSetMode_ReturnToOverview(t *testing.T) {
|
|||||||
assert.Equal(t, "auto", store.modeOf(1))
|
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) {
|
func TestSetMode_AutoRefusedShows409(t *testing.T) {
|
||||||
srv, store, _, _, jobs := newTestEnv(t)
|
srv, store, _, _, jobs := newTestEnv(t)
|
||||||
jobs.autoErr = &model.NotUpdatableError{Reason: model.ReasonBuilt}
|
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{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:hover{color:var(--fg)}
|
||||||
.seg button[aria-pressed=true]{background:var(--hover);color:var(--fg);box-shadow:inset 0 0 0 1px var(--line2)}
|
.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}
|
.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{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}
|
.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}}
|
{{if .Self}}<span class="tag f-out">This agent</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="mode">
|
<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="csrf_token" value="{{$.CSRF}}">
|
||||||
<input type="hidden" name="return" value="overview">
|
<input type="hidden" name="return" value="overview">
|
||||||
{{if eq .UpdateMode "auto"}}<input type="hidden" name="mode" value="manual">
|
{{if eq .UpdateMode "auto"}}<input type="hidden" name="mode" value="manual">
|
||||||
|
|||||||
Reference in New Issue
Block a user