diff --git a/Cargo.lock b/Cargo.lock index 7e05da6..773b4e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2710,7 +2710,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "local-dev-proxy" -version = "0.0.16" +version = "0.0.17" dependencies = [ "bytes", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 1559041..b6c5f22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "local-dev-proxy" -version = "0.0.16" +version = "0.0.17" edition = "2024" description = "Local development process orchestration with a built-in reverse proxy" authors = ["Andrew Chen "] diff --git a/README.md b/README.md index 0f48612..647b0dc 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,10 @@ the active file (`.log.1` is newest). This bounds each log to about 60 MiB. Rota renames completed files rather than truncating a file while it is being written; once the retention limit is reached, only the oldest backup is removed. -Edit the configuration in the manager window (**View Config → Stop All & Edit Config**, -then **Validate**, **Save**, or **Start All**), or edit the file by hand while the -application is stopped, check it with `local-dev-proxy --check-config`, and start the -application again. +Edit the configuration in the manager window (**Edit Config**, then **Validate**, +**Save**, or **Apply**) while everything keeps running, or edit the file by hand, check +it with `local-dev-proxy --check-config`, and apply it from the editor (or restart the +application). ## Usage @@ -121,11 +121,14 @@ service's combined stdout/stderr goes to `logs/.log`. The manager window has three tabs: - **Services** — each service's status, PID, restart count, and last exit code. Select a - row to start, stop, or restart it; double-click a row to open its log. **View Config** - shows the running configuration; **Stop All & Edit Config** stops everything and opens - the editor, where **Start All** validates, saves, and launches the edited file. If the - configuration fails to start, the editor opens with the error. Both views highlight - the TOML syntax. + row to start, stop, or restart it; double-click a row to open its log. **Edit Config** + opens the configuration editor (with TOML syntax highlighting) without stopping + anything; **Cancel** discards unsaved edits and goes back to the service list. **Save** + only writes the file; **Apply** validates and saves it, then restarts the proxy and + every service only if the configuration differs from the running one (comment and + formatting changes do not count). While stopped the button is **Start All** instead. If + the configuration fails to start, everything stays stopped and the editor opens with + the error. - **Logs** — the tail of a service's log, with a line count and **Follow** to keep it updating. - **Routes** — every service's hosts and targets. Click a URL to open it in the browser. @@ -140,11 +143,11 @@ absent or disabled: | Ctrl+Up / Ctrl+Down | Select the previous / next service | | Ctrl+Shift+S / Ctrl+Shift+X / Ctrl+Shift+R | Start / Stop / Restart the selected service | | Ctrl+L | Open the selected service's log | -| Ctrl+E | View Config, or back to the service list | -| Ctrl+Shift+E | Stop All & Edit Config | +| Ctrl+E | Edit Config | +| Escape | Cancel: discard unsaved edits and go back to the service list | | Ctrl+K / Ctrl+S | Validate / Save the configuration being edited | -| Ctrl+Enter | Start All (validate, save, and launch the edited configuration) | -| Ctrl+R | Reload what the current tab shows: the editor from disk, the log, or the routes | +| Ctrl+Enter | Apply (validate, save, and restart if it changed), or Start All while stopped | +| Ctrl+R | Reload what the current tab shows: the log or the routes | | Ctrl+Q | Quit | Only one instance runs per profile. Launching it again while it is running brings the @@ -303,9 +306,9 @@ disagree, the workflow is right and the script is stale. `ci/unix/e2e.sh` launches the real binary against a throwaway profile, on the headless labwc session of the development host, and drives the manager window through its -keyboard shortcuts: select, stop, start and restart a service, open its log, the routes -and the read-only configuration, stop everything to edit, save, validate, start again, -and quit. After each step it checks the proxy's answer over HTTP and the text on screen, +keyboard shortcuts: select, stop, start and restart a service, open its log and the +routes, edit the configuration while it keeps running, save, validate, apply it unchanged +(nothing restarts) and changed (everything restarts), and quit. After each step it checks the proxy's answer over HTTP and the text on screen, read from a screenshot with tesseract. `ci/unix/ci.sh` runs it on a Linux host where the session is up and skips it elsewhere; `ci/unix/e2e.sh --available` says which. It needs the labwc session's `DISPLAY` in the systemd user environment (or diff --git a/ci/unix/e2e.sh b/ci/unix/e2e.sh index 8b419c7..73ba1dc 100755 --- a/ci/unix/e2e.sh +++ b/ci/unix/e2e.sh @@ -7,9 +7,10 @@ # # The scenario launches the manager against a throwaway profile whose one # service is a busybox httpd, then walks every keyboard shortcut: select the -# service, stop, start and restart it, open its log, the routes and the -# read-only configuration, stop everything to edit, save, validate, start -# again, and quit. After each step it checks what a user would check: the +# service, stop, start and restart it, open its log and the routes, edit the +# configuration while it keeps running, cancel, edit again, save, validate, +# apply it unchanged (nothing restarts), apply a change (everything +# restarts), and quit. After each step it checks what a user would check: the # proxy's answer over HTTP, and the text on screen (a screenshot of the # session, read back with tesseract). tmp/e2e/ keeps the numbered screenshots # and the app's logs, for looking at after a failure. @@ -205,21 +206,36 @@ key ctrl+3 # The target column; tesseract skips the blue URL text. expect_screen routes "localhost:$web_port" key ctrl+1 -key ctrl+e -expect_screen config 'Viewing configuration' -# Stop everything and edit, then start it all again. -key ctrl+shift+e -expect_screen editing 'Editing configuration' -expect_http 'proxy down while editing' web.localhost 000 +# Edit while everything keeps running; applying it unchanged restarts nothing. +key ctrl+e +expect_screen editing 'services keep running' +expect_http 'proxy up while editing' web.localhost 200 +key Escape +expect_screen cancelled 'Edit Config' +key ctrl+e +expect_screen editing-again 'services keep running' key ctrl+s expect_screen saved '\bsaved\b' key ctrl+k # Word-bounded: the banner says "validates". expect_screen valid '\bvalid\b' key ctrl+Return -expect_screen restarted-all 'saved.{1,3}started' -expect_http 'started again' web.localhost 200 +expect_screen unchanged 'no changes' +expect_http 'still up after an unchanged apply' web.localhost 200 +grep -c 'web starting on' "$profile/logs/web.log" | grep -qx 3 || fail 'an unchanged apply restarted the service' + +# A changed configuration (written to disk; the editor loads it) restarts +# everything with it. +new_web_port=$(free_port) +sed -i "s/WEB_PORT = \"$web_port\"/WEB_PORT = \"$new_web_port\"/" "$profile/services.toml" +key ctrl+e +expect_screen edited "$new_web_port" +key ctrl+Return +expect_screen restarted-all 'saved.{1,3}restarted' +expect_http 'restarted with the change' web.localhost 200 +grep -q "web starting on $new_web_port" "$profile/logs/web.log" || fail 'the service did not restart on the new port' +web_port=$new_web_port # Quit takes the service down with it. key ctrl+q diff --git a/src/desktop.rs b/src/desktop.rs index 9d3376d..888d92c 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -16,7 +16,7 @@ use slint::{ }; use crate::frontend::{AppEvent, Frontend}; -use crate::manager::{Manager, ManagerError}; +use crate::manager::{Applied, Manager, ManagerError}; use crate::process::{ServiceSnapshot, ServiceStatus}; use crate::routes::{RouteGroup, ServiceKind}; @@ -187,16 +187,9 @@ impl Controller { fn bind(self: &Rc) { let window = &self.window; window.on_quit(self.handler(|c| c.quit())); - window.on_toggle_view_config(self.handler(|c| { - let view = c.state.borrow().view; - c.set_view(if view == ServiceView::Readonly { - ServiceView::Services - } else { - ServiceView::Readonly - }); - })); - window.on_stop_to_edit(self.handler(|c| c.stop_to_edit())); - window.on_start_all(self.handler(|c| c.start_all())); + window.on_edit_config(self.handler(|c| c.set_view(ServiceView::Edit))); + window.on_cancel_edit(self.handler(|c| c.cancel_edit())); + window.on_apply(self.handler(|c| c.apply())); window.on_validate(self.handler(|c| { c.validate(); })); @@ -205,7 +198,6 @@ impl Controller { c.set_status("saved ✓", Level::Success); } })); - window.on_reload_config(self.handler(|c| c.reload_config())); window.on_start_service(self.handler(|c| c.service_action(ServiceAction::Start))); window.on_stop_service(self.handler(|c| c.service_action(ServiceAction::Stop))); window.on_restart_service(self.handler(|c| c.service_action(ServiceAction::Restart))); @@ -425,6 +417,7 @@ impl Controller { changed }; if changed { + self.window.set_running(running); self.set_view(if running { ServiceView::Services } else { @@ -436,19 +429,19 @@ impl Controller { fn set_view(&self, view: ServiceView) { let banner = match view { ServiceView::Services => String::new(), - ServiceView::Readonly => { - self.load_config(); - "Viewing configuration (read-only) — services are still running.".to_owned() - } ServiceView::Edit => { self.window.set_current_tab(SERVICES_TAB); - if self.load_config() { - "Editing configuration — Start All validates, saves, and launches it." - .to_owned() - } else { + if !self.load_config() { "No services.toml exists yet. Enter a configuration, then Save or Start All \ to create it." .to_owned() + } else if self.window.get_running() { + "Editing configuration — services keep running. Apply validates and saves \ + it, and restarts everything only if the configuration changed." + .to_owned() + } else { + "Editing configuration — Start All validates, saves, and launches it." + .to_owned() } } }; @@ -517,29 +510,36 @@ impl Controller { // --- lifecycle -------------------------------------------------------------- - fn stop_to_edit(self: &Rc) { - self.job("stopping…".to_owned(), |manager| { - manager.stop(); - Ok("stopped — editing".to_owned()) - }); - } - - fn start_all(self: &Rc) { + /// Save the editor text, then start it, or restart with it only if it + /// differs from the running configuration. + fn apply(self: &Rc) { if !self.persist() { return; } - self.job("starting…".to_owned(), |manager| { + if self.window.get_running() { + // A failed restart leaves everything stopped, which brings the + // editor back. + self.set_view(ServiceView::Services); + } + self.job("applying…".to_owned(), |manager| { manager - .start() - .map(|_| "saved & started ✓".to_owned()) - .map_err(|err| format!("start failed: {err}")) + .apply() + .map(|applied| { + match applied { + Applied::Started => "saved & started ✓", + Applied::Restarted => "saved & restarted ✓", + Applied::Unchanged => "saved — no changes, nothing restarted", + } + .to_owned() + }) + .map_err(|err| format!("apply failed: {err}")) }); } // --- configuration -------------------------------------------------------------- - /// Load the configuration on disk into both config views; returns - /// whether a file exists. + /// Load the configuration on disk into the editor; returns whether a + /// file exists. fn load_config(&self) -> bool { let Some(result) = self.with_manager(Manager::read_config) else { return false; @@ -556,18 +556,18 @@ impl Controller { } }; self.window.set_editor_text(text.as_str().into()); - self.window.set_config_text(text.as_str().into()); self.window.set_dirty(false); self.state.borrow_mut().loaded_config = text; exists } - fn reload_config(&self) { - let exists = self.load_config(); - self.restore_mode_banner(); - if exists { - self.set_status("reloaded from disk", Level::Neutral); + /// Discard unsaved edits and go back to the service list. + fn cancel_edit(&self) { + if self.window.get_dirty() { + self.set_status("edits discarded", Level::Neutral); } + self.load_config(); + self.set_view(ServiceView::Services); } fn validate(&self) -> bool { @@ -607,10 +607,6 @@ impl Controller { self.set_banner(&err.to_string(), Level::Error); false } - Err(ManagerError::StillRunning) => { - self.set_status("stop services first", Level::Error); - false - } Err(err) => { self.set_status(&err.to_string(), Level::Error); false @@ -880,7 +876,7 @@ mod tests { /// is the only test in the crate that creates Slint components. #[cfg(unix)] #[test] - fn manager_window_edits_starts_controls_and_stops_services() { + fn manager_window_edits_starts_controls_and_applies_services() { use i_slint_backend_testing::ElementHandle; use slint::Model; @@ -935,12 +931,26 @@ mod tests { .local_addr() .unwrap() .port(); - let config = format!( - "http_port = {port}\nbind = [\"127.0.0.1\"]\n\n\ - [services.app]\ncommand = [\"sh\", \"-c\", \"echo hello from app; exec sleep 30\"]\n\n\ - [[services.app.routes]]\nid = \"app\"\nhosts = [\"app.localhost\"]\ntarget_port = 1\n\n\ - [services.external]\n" - ); + let config_on = |port: u16| { + format!( + "http_port = {port}\nbind = [\"127.0.0.1\"]\n\n\ + [services.app]\ncommand = [\"sh\", \"-c\", \"echo hello from app; exec sleep 30\"]\n\n\ + [[services.app.routes]]\nid = \"app\"\nhosts = [\"app.localhost\"]\ntarget_port = 1\n\n\ + [services.external]\n" + ) + }; + let config = config_on(port); + // The changed configuration must differ, so the port must too. + let new_port = loop { + let candidate = std::net::TcpListener::bind("127.0.0.1:0") + .unwrap() + .local_addr() + .unwrap() + .port(); + if candidate != port { + break candidate; + } + }; let controller = Controller::new(Arc::clone(&manager), ManagerWindow::new().unwrap(), None); controller.bind(); @@ -959,6 +969,14 @@ mod tests { .starts_with("No services.toml exists yet") ); + // Cancel goes back to the service list while stopped, too. + type_config(window, "# discarded\n"); + click(window, "Cancel"); + assert_eq!(window.get_view(), ServiceView::Services); + click(window, "Edit Config"); + assert_eq!(window.get_view(), ServiceView::Edit); + assert_eq!(window.get_editor_text(), ""); + type_config(window, "http_port = \"x\"\nbind = [\"127.0.0.1\"]"); assert!(window.get_dirty()); click(window, "Validate"); @@ -1026,17 +1044,53 @@ mod tests { }) .await; + // Editing leaves the services running. + let pid = window.get_services().row_data(0).unwrap().pid; + window.set_current_tab(LOGS_TAB); window.set_current_tab(SERVICES_TAB); - click(window, "View Config"); - assert_eq!(window.get_view(), ServiceView::Readonly); - assert_eq!(window.get_config_text(), config.as_str()); - - click(window, "Stop All & Edit Config"); - wait_until("stop", || !window.get_busy()).await; - assert_eq!(window.get_status_text(), "stopped — editing"); + click(window, "Edit Config"); assert_eq!(window.get_view(), ServiceView::Edit); - assert_eq!(window.get_current_tab(), SERVICES_TAB); - assert_eq!(window.get_services().row_data(0).unwrap().status, "stopped"); + assert_eq!(window.get_editor_text(), config.as_str()); + assert!(window.get_banner().contains("services keep running")); + assert!(window.get_running()); + + // Cancel discards unsaved edits and goes back to the list. + type_config(window, "# discarded\n"); + assert!(window.get_dirty()); + click(window, "Cancel"); + assert_eq!(window.get_view(), ServiceView::Services); + assert_eq!(window.get_status_text(), "edits discarded"); + assert_eq!(std::fs::read_to_string(&services_file).unwrap(), config); + click(window, "Edit Config"); + assert!(!window.get_dirty()); + assert_eq!(window.get_editor_text(), config.as_str()); + type_config(window, &format!("# only a comment\n{config}")); + + // A comment is no change: saved, but nothing restarts. + click(window, "Apply"); + wait_until("apply", || !window.get_busy()).await; + assert_eq!( + window.get_status_text(), + "saved — no changes, nothing restarted" + ); + assert_eq!(window.get_view(), ServiceView::Services); + assert!( + std::fs::read_to_string(&services_file) + .unwrap() + .starts_with("# only a comment") + ); + assert_eq!(window.get_services().row_data(0).unwrap().pid, pid); + + // A real change restarts everything with it. + click(window, "Edit Config"); + type_config(window, &config_on(new_port)); + click(window, "Apply"); + wait_until("apply", || !window.get_busy()).await; + assert_eq!(window.get_status_text(), "saved & restarted ✓"); + assert_eq!(window.get_view(), ServiceView::Services); + assert_ne!(window.get_services().row_data(0).unwrap().pid, pid); + std::net::TcpStream::connect(("127.0.0.1", new_port)).unwrap(); + assert!(std::net::TcpStream::connect(("127.0.0.1", port)).is_err()); click(window, "Quit"); } @@ -1044,8 +1098,11 @@ mod tests { slint::spawn_local(script).unwrap(); slint::run_event_loop_until_quit().unwrap(); - let manager = lock(&manager); - assert!(!manager.is_running()); + // Quitting only ends the event loop; `Desktop::run` then stops + // everything, as here. + let mut manager = lock(&manager); + assert!(manager.is_running()); + manager.stop(); assert!( manager .services() diff --git a/src/manager.rs b/src/manager.rs index c09b2bd..ce0fe10 100644 --- a/src/manager.rs +++ b/src/manager.rs @@ -24,8 +24,6 @@ pub enum ManagerError { Service(#[from] ServiceError), #[error("services are not running")] NotRunning, - #[error("stop services before changing the configuration")] - StillRunning, #[error("could not write {path}: {source}")] Write { path: String, @@ -41,6 +39,20 @@ pub struct Manager { /// Kept after a stop so the last known service states stay visible. services: Option, proxy: Option, + /// The configuration the proxy and services were started from; `Some` + /// exactly while running. + running: Option, +} + +/// What [`Manager::apply`] did with the configuration on disk. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Applied { + /// Nothing was running, so it was started. + Started, + /// It differed from the running configuration, so everything restarted. + Restarted, + /// It matches the running configuration; nothing was touched. + Unchanged, } impl Manager { @@ -55,6 +67,7 @@ impl Manager { log_limits: LogLimits::default(), services: None, proxy: None, + running: None, }) } @@ -107,6 +120,7 @@ impl Manager { )) { Ok(proxy) => { self.proxy = Some(proxy); + self.running = Some(manifest.clone()); Ok(manifest) } Err(err) => { @@ -116,8 +130,24 @@ impl Manager { } } + /// Start the configuration on disk, restarting everything only if it + /// differs from the running one. Comment and formatting changes do not + /// count as differences. + pub fn apply(&mut self) -> Result { + let Some(running) = &self.running else { + self.start()?; + return Ok(Applied::Started); + }; + if *running == load_manifest(&self.paths.services_file())? { + return Ok(Applied::Unchanged); + } + self.start()?; + Ok(Applied::Restarted) + } + /// Stop the proxy and every managed service. pub fn stop(&mut self) { + self.running = None; if let Some(proxy) = self.proxy.take() { self.runtime.block_on(proxy.shutdown()); } @@ -167,9 +197,13 @@ impl Manager { Ok(tail_file(&services.log_path(name)?, lines)) } - /// Route listing for the configuration on disk. + /// Route listing for the running configuration, or the one on disk + /// while stopped. pub fn routes(&self) -> Result, ConfigError> { - Ok(route_listing(&load_manifest(&self.paths.services_file())?)) + match &self.running { + Some(manifest) => Ok(route_listing(manifest)), + None => Ok(route_listing(&load_manifest(&self.paths.services_file())?)), + } } /// The configuration text on disk, or `None` if none exists yet. @@ -189,12 +223,9 @@ impl Manager { Ok(manifest) } - /// Validate and atomically replace the configuration. Only allowed while - /// stopped, so the running state always matches the file it came from. + /// Validate and atomically replace the configuration. The running state + /// is untouched until [`Manager::apply`]. pub fn save_config(&self, text: &str) -> Result<(), ManagerError> { - if self.is_running() { - return Err(ManagerError::StillRunning); - } self.validate_config(text)?; write_atomically(&self.paths.services_file(), text) } @@ -247,7 +278,7 @@ mod tests { } #[test] - fn config_editing_is_only_allowed_while_stopped() { + fn config_edits_while_running_apply_only_when_they_change_something() { let (_dir, mut manager) = manager(); assert_eq!(manager.read_config().unwrap(), None); assert!(matches!( @@ -256,20 +287,36 @@ mod tests { )); assert!(!manager.has_config()); - let text = config(free_port()); + let first_port = free_port(); + let text = config(first_port); manager.save_config(&text).unwrap(); assert_eq!( manager.read_config().unwrap().as_deref(), Some(text.as_str()) ); + assert_eq!(manager.apply().unwrap(), Applied::Started); + let pid = manager.services()[0].pid; - manager.start().unwrap(); - assert!(matches!( - manager.save_config(&text), - Err(ManagerError::StillRunning) - )); - manager.stop(); - manager.save_config(&text).unwrap(); + // Saving while running leaves the running services alone. + let commented = format!("# a comment\n{text}"); + manager.save_config(&commented).unwrap(); + assert!(manager.is_running()); + assert_eq!(manager.apply().unwrap(), Applied::Unchanged); + assert_eq!(manager.services()[0].pid, pid); + + // The changed configuration must differ, so the port must too. + let port = loop { + let candidate = free_port(); + if candidate != first_port { + break candidate; + } + }; + manager.save_config(&config(port)).unwrap(); + assert_eq!(manager.services()[0].pid, pid); + assert_eq!(manager.apply().unwrap(), Applied::Restarted); + assert!(manager.is_running()); + assert_ne!(manager.services()[0].pid, pid); + std::net::TcpStream::connect(("127.0.0.1", port)).unwrap(); } #[test] diff --git a/ui/app.slint b/ui/app.slint index 0ad7abe..06a8bcf 100644 --- a/ui/app.slint +++ b/ui/app.slint @@ -1,6 +1,6 @@ import { Button, CheckBox, ComboBox, ListView, Palette, ScrollView, SpinBox, TabWidget, TextEdit } from "std-widgets.slint"; -export enum ServiceView { services, readonly, edit } +export enum ServiceView { services, edit } export enum Level { neutral, success, error } @@ -456,6 +456,8 @@ export component ManagerWindow inherits Window { in property mono-font; /// A lifecycle or service action is running in the background. in property busy; + /// The proxy and services are running. + in property running; in-out property current-tab; in property view; @@ -465,7 +467,6 @@ export component ManagerWindow inherits Window { in property status-level; in property <[ServiceRow]> services; in property selected-service: -1; - in property config-text; in-out property editor-text; in property dirty; @@ -479,12 +480,11 @@ export component ManagerWindow inherits Window { in property routes-banner; callback quit(); - callback toggle-view-config(); - callback stop-to-edit(); - callback start-all(); + callback edit-config(); + callback cancel-edit(); + callback apply(); callback validate(); callback save(); - callback reload-config(); callback editor-edited(string); callback service-selected(int); callback start-service(); @@ -518,7 +518,6 @@ export component ManagerWindow inherits Window { private property on-services-tab: current-tab == 0; private property in-service-list: on-services-tab && view == ServiceView.services; private property editing: on-services-tab && view == ServiceView.edit; - private property viewing-config: on-services-tab && view == ServiceView.readonly; function move-selection(delta: int) { if (in-service-list && services.length > 0) { @@ -528,11 +527,9 @@ export component ManagerWindow inherits Window { // Reload whatever the current tab shows. function refresh-current-tab() { - if (editing && !busy) { - root.reload-config(); - } else if (current-tab == 1 && view != ServiceView.edit) { + if (current-tab == 1 && running) { root.refresh-logs(); - } else if (current-tab == 2 && view != ServiceView.edit) { + } else if (current-tab == 2 && running) { root.reload-routes(); } } @@ -594,17 +591,17 @@ export component ManagerWindow inherits Window { KeyBinding { keys: @keys(Control + "e"); activated => { - if ((root.in-service-list || root.viewing-config) && !root.busy) { - root.toggle-view-config(); + if (root.in-service-list && !root.busy) { + root.edit-config(); } } } KeyBinding { - keys: @keys(Control + Shift + "e"); + keys: @keys(Escape); activated => { - if (root.viewing-config && !root.busy) { - root.stop-to-edit(); + if (root.editing && !root.busy) { + root.cancel-edit(); } } } @@ -613,7 +610,7 @@ export component ManagerWindow inherits Window { keys: @keys(Control + Return); activated => { if (root.editing && !root.busy) { - root.start-all(); + root.apply(); } } } @@ -731,26 +728,19 @@ export component ManagerWindow inherits Window { HorizontalLayout { spacing: 8px; - if root.view != ServiceView.edit: Button { - text: root.view == ServiceView.readonly ? "Back to Services" : "View Config"; - enabled: !root.busy; - clicked => { - root.toggle-view-config(); - } - } - if root.view == ServiceView.readonly: Button { - text: "Stop All & Edit Config"; + if root.view == ServiceView.services: Button { + text: "Edit Config"; enabled: !root.busy; clicked => { - root.stop-to-edit(); + root.edit-config(); } } if root.view == ServiceView.edit: Button { - text: "Start All"; + text: root.running ? "Apply" : "Start All"; primary: true; enabled: !root.busy; clicked => { - root.start-all(); + root.apply(); } } Rectangle { } @@ -820,14 +810,6 @@ export component ManagerWindow inherits Window { } } - if root.view == ServiceView.readonly: TomlEdit { - accessible-label: "Configuration"; - vertical-stretch: 1; - read-only: true; - font-family: root.mono-font; - text: root.config-text; - } - if root.view == ServiceView.edit: VerticalLayout { spacing: 8px; TomlEdit { @@ -860,10 +842,10 @@ export component ManagerWindow inherits Window { } Button { - text: "Reload from disk"; + text: "Cancel"; enabled: !root.busy; clicked => { - root.reload-config(); + root.cancel-edit(); } } @@ -881,14 +863,14 @@ export component ManagerWindow inherits Window { Tab { title: "Logs"; - if root.view == ServiceView.edit: Text { - text: "Unavailable while editing the configuration."; + if !root.running: Text { + text: "Unavailable while services are stopped."; color: Colors.muted; horizontal-alignment: center; vertical-alignment: center; } - if root.view != ServiceView.edit: VerticalLayout { + if root.running: VerticalLayout { padding: 12px; spacing: 8px; HorizontalLayout { @@ -954,14 +936,14 @@ export component ManagerWindow inherits Window { Tab { title: "Routes"; - if root.view == ServiceView.edit: Text { - text: "Unavailable while editing the configuration."; + if !root.running: Text { + text: "Unavailable while services are stopped."; color: Colors.muted; horizontal-alignment: center; vertical-alignment: center; } - if root.view != ServiceView.edit: VerticalLayout { + if root.running: VerticalLayout { padding: 12px; spacing: 8px; Text {