Skip to content

Commit a47e141

Browse files
committed
webui: only flag unsaved changes when running differs from startup
The "unsaved changes" banner was set unconditionally at every site that touches config, so it could appear when running-config actually equals startup — contradicting the "Show diff" modal, which correctly showed nothing. A shared updateCfgUnsaved helper now compares running against startup (the same datastore pair the diff uses) and sets or clears the banner cookie to match: - Apply and restore-to-running clear it when an Apply/restore reverts an out-of-band (e.g. CLI) change back to match startup, instead of always showing it. - The advanced-tree presence toggle writes only the candidate datastore, so it no longer sets the running-vs-startup banner at all (matching the other candidate-only tree handlers). Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
1 parent 93db076 commit a47e141

3 files changed

Lines changed: 41 additions & 11 deletions

File tree

src/webui/internal/handlers/configure.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,49 @@ func applyError(w http.ResponseWriter, op string, err error) {
109109
http.Error(w, "Could not reach the device: "+err.Error(), http.StatusBadGateway)
110110
}
111111

112-
// Apply copies candidate → running, activating all staged changes atomically.
113-
// Sets the cfg-unsaved cookie so the persistent banner appears until startup is saved.
112+
// Apply copies candidate → running, activating all staged changes atomically,
113+
// then updates the unsaved-changes banner to match the running-vs-startup state.
114114
// POST /configure/apply
115115
func (h *ConfigureHandler) Apply(w http.ResponseWriter, r *http.Request) {
116116
if err := h.RC.CopyDatastore(r.Context(), "candidate", "running"); err != nil {
117117
applyError(w, "apply", err)
118118
return
119119
}
120-
setCfgUnsaved(w)
120+
updateCfgUnsaved(r.Context(), h.RC, w)
121121
w.Header().Set("HX-Refresh", "true")
122122
w.WriteHeader(http.StatusNoContent)
123123
}
124124

125+
// configPair fetches the startup and running datastores — the shared basis for
126+
// the unsaved-state check (updateCfgUnsaved) and the config diff (configDiff).
127+
func configPair(ctx context.Context, rc restconf.Fetcher) (startup, running json.RawMessage, err error) {
128+
if startup, err = rc.GetDatastore(ctx, "startup"); err != nil {
129+
return nil, nil, fmt.Errorf("read startup-config: %w", err)
130+
}
131+
if running, err = rc.GetDatastore(ctx, "running"); err != nil {
132+
return nil, nil, fmt.Errorf("read running-config: %w", err)
133+
}
134+
return startup, running, nil
135+
}
136+
137+
// updateCfgUnsaved sets or clears the cfg-unsaved banner cookie to match the
138+
// actual state: set when running-config differs from startup, cleared when they
139+
// are byte-identical (an Apply or restore can revert an out-of-band change so
140+
// nothing is unsaved). Byte comparison is reliable given the shared serializer
141+
// (the basis the config diff uses); a read error fails open and shows the
142+
// banner. Call after any operation that writes running-config.
143+
func updateCfgUnsaved(ctx context.Context, rc restconf.Fetcher, w http.ResponseWriter) {
144+
startup, running, err := configPair(ctx, rc)
145+
if err != nil {
146+
log.Printf("configure: unsaved-state check: %v", err)
147+
}
148+
if err == nil && bytes.Equal(running, startup) {
149+
clearCfgUnsaved(w)
150+
} else {
151+
setCfgUnsaved(w)
152+
}
153+
}
154+
125155
// Abort copies running → candidate, discarding all staged changes.
126156
// POST /configure/abort
127157
func (h *ConfigureHandler) Abort(w http.ResponseWriter, r *http.Request) {
@@ -188,13 +218,9 @@ func (h *ConfigureHandler) ConfigDiff(w http.ResponseWriter, r *http.Request) {
188218
// configDiff fetches startup and running config, writes each to a temp file,
189219
// and returns the `diff -u` output (empty when the two are identical).
190220
func (h *ConfigureHandler) configDiff(ctx context.Context) (string, error) {
191-
startup, err := h.RC.GetDatastore(ctx, "startup")
221+
startup, running, err := configPair(ctx, h.RC)
192222
if err != nil {
193-
return "", fmt.Errorf("read startup-config: %w", err)
194-
}
195-
running, err := h.RC.GetDatastore(ctx, "running")
196-
if err != nil {
197-
return "", fmt.Errorf("read running-config: %w", err)
223+
return "", err
198224
}
199225

200226
startupFile, err := writeTempConfig("cfgdiff-startup-*.json", startup)

src/webui/internal/handlers/system.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,9 @@ func (h *SystemHandler) RestoreConfig(w http.ResponseWriter, r *http.Request) {
395395
}
396396

397397
if target == "running" {
398-
setCfgUnsaved(w)
398+
// Restoring the same config that's already in startup leaves nothing
399+
// unsaved, so reflect the actual running-vs-startup state.
400+
updateCfgUnsaved(r.Context(), h.RC, w)
399401
w.Header().Set("HX-Refresh", "true")
400402
w.WriteHeader(http.StatusNoContent)
401403
return

src/webui/internal/handlers/yang_tree.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1728,7 +1728,9 @@ func (h *TreeHandler) TogglePresence(w http.ResponseWriter, r *http.Request) {
17281728
exists = true
17291729
}
17301730

1731-
setCfgUnsaved(w)
1731+
// No setCfgUnsaved here — this only modifies candidate. The cfg-unsaved
1732+
// banner is specifically about "running differs from startup," set after
1733+
// Apply / RestoreConfig-to-running (matching the other tree-edit handlers).
17321734

17331735
// Re-render the right-pane leaf group with the updated presence state.
17341736
gd := h.buildLeafGroup(r, mgr, path, node.Name, "container")

0 commit comments

Comments
 (0)