From a410710ae8bd4638e2f1bb0d27657cd3ea5b85e0 Mon Sep 17 00:00:00 2001 From: JMit-dev Date: Wed, 30 Jul 2025 09:28:04 -0400 Subject: [PATCH 1/8] feat: added ui events for plans and table items --- app/queue-server/pom.xml | 2 +- .../queueserver/view/ItemUpdateEvent.java | 34 +++++++++++++++ .../queueserver/view/PlanEditEvent.java | 41 +++++++++++++++++++ .../UiSignalEvent.java} | 6 +-- 4 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java create mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java rename app/queue-server/src/main/java/org/phoebus/applications/queueserver/{util/UiSignals.java => view/UiSignalEvent.java} (72%) diff --git a/app/queue-server/pom.xml b/app/queue-server/pom.xml index 77e394bd1d..905009aa7a 100644 --- a/app/queue-server/pom.xml +++ b/app/queue-server/pom.xml @@ -35,4 +35,4 @@ 2.19.1 - + \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java new file mode 100644 index 0000000000..f0700c5581 --- /dev/null +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/ItemUpdateEvent.java @@ -0,0 +1,34 @@ +package org.phoebus.applications.queueserver.view; + +import org.phoebus.applications.queueserver.api.QueueItem; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Logger; +import java.util.logging.Level; + +public class ItemUpdateEvent { + private static final ItemUpdateEvent instance = new ItemUpdateEvent(); + private final List> listeners = new ArrayList<>(); + private static final Logger LOG = Logger.getLogger(ItemUpdateEvent.class.getName()); + + private ItemUpdateEvent() {} + + public static ItemUpdateEvent getInstance() { + return instance; + } + + public void addListener(Consumer listener) { + listeners.add(listener); + } + + public void notifyItemUpdated(QueueItem updatedItem) { + for (Consumer listener : listeners) { + try { + listener.accept(updatedItem); + } catch (Exception e) { + LOG.log(Level.WARNING, "Item update listener error", e); + } + } + } +} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java new file mode 100644 index 0000000000..fa31cb08c2 --- /dev/null +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/PlanEditEvent.java @@ -0,0 +1,41 @@ +package org.phoebus.applications.queueserver.view; + +import org.phoebus.applications.queueserver.api.QueueItem; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class PlanEditEvent { + + private static final PlanEditEvent INSTANCE = new PlanEditEvent(); + + private final List> listeners = new CopyOnWriteArrayList<>(); + private static final Logger LOG = Logger.getLogger(PlanEditEvent.class.getName()); + + private PlanEditEvent() {} + + public static PlanEditEvent getInstance() { + return INSTANCE; + } + + public void addListener(Consumer listener) { + listeners.add(listener); + } + + public void removeListener(Consumer listener) { + listeners.remove(listener); + } + + public void notifyEditRequested(QueueItem itemToEdit) { + for (Consumer listener : listeners) { + try { + listener.accept(itemToEdit); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error in plan edit listener", e); + } + } + } +} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/UiSignals.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/UiSignalEvent.java similarity index 72% rename from app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/UiSignals.java rename to app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/UiSignalEvent.java index 70f081494d..3beb73e729 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/util/UiSignals.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/UiSignalEvent.java @@ -1,14 +1,14 @@ -package org.phoebus.applications.queueserver.util; +package org.phoebus.applications.queueserver.view; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -public final class UiSignals { +public final class UiSignalEvent { private static final BooleanProperty ENV_DESTROY_ARMED = new SimpleBooleanProperty(false); - private UiSignals() {} + private UiSignalEvent() {} public static BooleanProperty envDestroyArmedProperty() { return ENV_DESTROY_ARMED; From 37c0627dbfbd5b53d4c4f734436e266e8e7c3213 Mon Sep 17 00:00:00 2001 From: JMit-dev Date: Wed, 30 Jul 2025 09:34:41 -0400 Subject: [PATCH 2/8] feat: queue item plan creation method --- .../queueserver/client/RunEngineService.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java index 5d1544591b..5e42902b49 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/client/RunEngineService.java @@ -34,9 +34,9 @@ public final class RunEngineService { /* ---- Ping & status --------------------------------------------------- */ public Envelope ping() throws Exception { return http.call(ApiEndpoint.PING, NoBody.INSTANCE); } - public StatusResponse status() throws Exception { + public StatusResponse status() throws Exception { LOG.log(Level.FINEST, "Fetching status"); - return http.send(ApiEndpoint.STATUS, NoBody.INSTANCE, StatusResponse.class); + return http.send(ApiEndpoint.STATUS, NoBody.INSTANCE, StatusResponse.class); } public Envelope configGet() throws Exception { return http.call(ApiEndpoint.CONFIG_GET, NoBody.INSTANCE); } @@ -136,6 +136,22 @@ public Envelope queueItemAddBatch(List items, public Envelope queueItemAddBatch(QueueItemMoveBatch body ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_ADD_BATCH, body); } public Envelope queueItemGet(Object params ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_GET, params); } public Envelope queueItemUpdate(Object b ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_UPDATE, b); } + + public Envelope queueItemUpdate(QueueItem item) throws Exception { + Map updateRequest = Map.of( + "item", Map.of( + "item_type", item.itemType(), + "name", item.name(), + "args", item.args(), + "kwargs", item.kwargs(), + "item_uid", item.itemUid() + ), + "user", item.user() != null ? item.user() : "GUI Client", + "user_group", item.userGroup() != null ? item.userGroup() : "primary", + "replace", true + ); + return queueItemUpdate(updateRequest); + } public Envelope queueItemRemove(Object b ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_REMOVE, b); } public Envelope queueItemRemoveBatch(Object b) throws Exception {return http.call(ApiEndpoint.QUEUE_ITEM_REMOVE_BATCH,b); } public Envelope queueItemMove(Object b ) throws Exception { return http.call(ApiEndpoint.QUEUE_ITEM_MOVE, b); } @@ -234,9 +250,9 @@ public Envelope rePause(String option) throws Exception { /* ---- Permissions & allowed lists ------------------------------------ */ public Envelope plansAllowed() throws Exception { return http.call(ApiEndpoint.PLANS_ALLOWED, NoBody.INSTANCE); } - public Map plansAllowedRaw() throws Exception { + public Map plansAllowedRaw() throws Exception { LOG.log(Level.FINE, "Fetching plans allowed (raw)"); - return http.send(ApiEndpoint.PLANS_ALLOWED, NoBody.INSTANCE); + return http.send(ApiEndpoint.PLANS_ALLOWED, NoBody.INSTANCE); } public Envelope devicesAllowed() throws Exception { return http.call(ApiEndpoint.DEVICES_ALLOWED, NoBody.INSTANCE); } public Envelope plansExisting() throws Exception { return http.call(ApiEndpoint.PLANS_EXISTING, NoBody.INSTANCE); } @@ -276,4 +292,4 @@ public Map plansAllowedRaw() throws Exception { public Envelope whoAmI() throws Exception { return http.call(ApiEndpoint.WHOAMI, NoBody.INSTANCE); } public Envelope apiScopes() throws Exception { return http.call(ApiEndpoint.API_SCOPES, NoBody.INSTANCE); } public Envelope logout() throws Exception { return http.call(ApiEndpoint.LOGOUT, NoBody.INSTANCE); } -} +} \ No newline at end of file From ea4ad0453da0041e7ad709622bcf99c138a8ff02 Mon Sep 17 00:00:00 2001 From: JMit-dev Date: Wed, 30 Jul 2025 09:40:28 -0400 Subject: [PATCH 3/8] feat: plan viewer/editor tab switch and queue item events --- .../view/QueueItemSelectionEvent.java | 42 +++++++++++++++++++ .../queueserver/view/TabSwitchEvent.java | 33 +++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/QueueItemSelectionEvent.java create mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/QueueItemSelectionEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/QueueItemSelectionEvent.java new file mode 100644 index 0000000000..0ce51e3286 --- /dev/null +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/QueueItemSelectionEvent.java @@ -0,0 +1,42 @@ +package org.phoebus.applications.queueserver.util; + +import org.phoebus.applications.queueserver.api.QueueItem; +import org.phoebus.applications.queueserver.view.TabSwitchEvent; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class QueueItemSelectionEvent { + + private static final QueueItemSelectionEvent INSTANCE = new QueueItemSelectionEvent(); + + private final List> listeners = new CopyOnWriteArrayList<>(); + private static final Logger LOG = Logger.getLogger(TabSwitchEvent.class.getName()); + + private QueueItemSelectionEvent() {} + + public static QueueItemSelectionEvent getInstance() { + return INSTANCE; + } + + public void addListener(Consumer listener) { + listeners.add(listener); + } + + public void removeListener(Consumer listener) { + listeners.remove(listener); + } + + public void notifySelectionChanged(QueueItem selectedItem) { + for (Consumer listener : listeners) { + try { + listener.accept(selectedItem); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error in queue selection listener", e); + } + } + } +} \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java new file mode 100644 index 0000000000..61069a74d2 --- /dev/null +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/view/TabSwitchEvent.java @@ -0,0 +1,33 @@ +package org.phoebus.applications.queueserver.view; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.logging.Logger; +import java.util.logging.Level; + +public class TabSwitchEvent { + private static final TabSwitchEvent instance = new TabSwitchEvent(); + private final List> listeners = new ArrayList<>(); + private static final Logger LOG = Logger.getLogger(TabSwitchEvent.class.getName()); + + private TabSwitchEvent() {} + + public static TabSwitchEvent getInstance() { + return instance; + } + + public void addListener(Consumer listener) { + listeners.add(listener); + } + + public void switchToTab(String tabName) { + for (Consumer listener : listeners) { + try { + listener.accept(tabName); + } catch (Exception e) { + LOG.log(Level.WARNING, "Tab switch listener error", e); + } + } + } +} \ No newline at end of file From e922bc74c33ac5f7a08c50122cf683c7ae3eb4ad Mon Sep 17 00:00:00 2001 From: JMit-dev Date: Wed, 30 Jul 2025 09:42:41 -0400 Subject: [PATCH 4/8] feat: plan manager tab switch controller --- .../controller/RePlanManagerController.java | 38 +++++++++++++++ .../queueserver/view/RePlanManager.fxml | 48 +++++++++---------- 2 files changed, 62 insertions(+), 24 deletions(-) create mode 100644 app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanManagerController.java diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanManagerController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanManagerController.java new file mode 100644 index 0000000000..8d2fd7ab5f --- /dev/null +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanManagerController.java @@ -0,0 +1,38 @@ +package org.phoebus.applications.queueserver.controller; + +import org.phoebus.applications.queueserver.view.TabSwitchEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; + +import java.net.URL; +import java.util.ResourceBundle; + +public class RePlanManagerController implements Initializable { + + @FXML private TabPane tabPane; + @FXML private Tab planViewerTab; + @FXML private Tab planEditorTab; + + @Override + public void initialize(URL location, ResourceBundle resources) { + TabSwitchEvent.getInstance().addListener(this::switchToTab); + } + + private void switchToTab(String tabName) { + if (tabPane == null) return; + + switch (tabName) { + case "Plan Editor" -> tabPane.getSelectionModel().select(planEditorTab); + case "Plan Viewer" -> tabPane.getSelectionModel().select(planViewerTab); + } + } + + public String getCurrentTabName() { + if (tabPane == null || tabPane.getSelectionModel().getSelectedItem() == null) { + return null; + } + return tabPane.getSelectionModel().getSelectedItem().getText(); + } +} \ No newline at end of file diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanManager.fxml b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanManager.fxml index 7e09f60529..1cca048396 100644 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanManager.fxml +++ b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanManager.fxml @@ -4,27 +4,27 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From aea6dd9adf8802d9d4afa1b0cc607d6003d6ae90 Mon Sep 17 00:00:00 2001 From: JMit-dev Date: Wed, 30 Jul 2025 09:46:00 -0400 Subject: [PATCH 5/8] feat: plan queue controller queue item selection event --- .../controller/RePlanQueueController.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java index 77508080bb..b1c2183264 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanQueueController.java @@ -6,6 +6,7 @@ import org.phoebus.applications.queueserver.api.StatusResponse; import org.phoebus.applications.queueserver.client.RunEngineService; import org.phoebus.applications.queueserver.util.StatusBus; +import org.phoebus.applications.queueserver.util.QueueItemSelectionEvent; import javafx.application.Platform; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyStringWrapper; @@ -97,7 +98,11 @@ public RePlanQueueController(boolean viewOnly) { table.getSelectionModel().getSelectedItems() .addListener((ListChangeListener) c -> { - if (!ignoreSticky) stickySel = selectedUids(); + if (!ignoreSticky) { + stickySel = selectedUids(); + // Notify plan viewer of selection change + notifySelectionChange(); + } }); ChangeListener poll = @@ -366,4 +371,17 @@ private static String fmtParams(QueueItem q) { } private record Row(String uid, String itemType, String name, String params, String user, String group) {} + + + private void notifySelectionChange() { + var selectedItems = table.getSelectionModel().getSelectedItems(); + QueueItem selectedItem = null; + + if (selectedItems.size() == 1) { + Row selectedRow = selectedItems.get(0); + selectedItem = uid2item.get(selectedRow.uid()); + } + + QueueItemSelectionEvent.getInstance().notifySelectionChanged(selectedItem); + } } \ No newline at end of file From 45d4a17fa58ff6000d98ba656fd7ce2799f2ee2b Mon Sep 17 00:00:00 2001 From: JMit-dev Date: Wed, 30 Jul 2025 10:07:35 -0400 Subject: [PATCH 6/8] feat: plan viewer and editor --- .../controller/RePlanEditorController.java | 729 +++++++++++++----- .../controller/RePlanViewerController.java | 477 +++++++++++- 2 files changed, 997 insertions(+), 209 deletions(-) diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java index 88456eafb6..72bf71aa5a 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanEditorController.java @@ -1,8 +1,11 @@ package org.phoebus.applications.queueserver.controller; -import org.phoebus.applications.queueserver.api.QueueItem; -import org.phoebus.applications.queueserver.api.QueueItemAdd; +import org.phoebus.applications.queueserver.api.*; import org.phoebus.applications.queueserver.client.RunEngineService; +import org.phoebus.applications.queueserver.view.PlanEditEvent; +import org.phoebus.applications.queueserver.view.TabSwitchEvent; +import org.phoebus.applications.queueserver.view.ItemUpdateEvent; +import com.fasterxml.jackson.databind.ObjectMapper; import javafx.application.Platform; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; @@ -35,7 +38,7 @@ public class RePlanEditorController implements Initializable { @FXML private Button saveBtn; @FXML private TableView table; @FXML private TableColumn valueCol; - + private final RunEngineService svc = new RunEngineService(); private static final Logger LOG = Logger.getLogger(RePlanEditorController.class.getName()); private final ObservableList parameterRows = FXCollections.observableArrayList(); @@ -45,22 +48,26 @@ public class RePlanEditorController implements Initializable { private boolean isEditMode = false; private String currentUser = "GUI Client"; private String currentUserGroup = "root"; + private String currentItemSource = ""; + private boolean editorStateValid = false; + private final ObjectMapper objectMapper = new ObjectMapper(); + // Store original parameter values for reset functionality + private final Map originalParameterValues = new HashMap<>(); - // Custom editable table cell that works like Python Qt - immediate editing on click private class EditableTableCell extends TableCell { private TextField textField; - + @Override protected void updateItem(String item, boolean empty) { super.updateItem(item, empty); - + if (empty || getTableRow() == null || getTableRow().getItem() == null) { setText(null); setGraphic(null); setStyle(""); } else { ParameterRow row = getTableRow().getItem(); - + if (isEditing()) { if (textField != null) { textField.setText(getString()); @@ -70,17 +77,25 @@ protected void updateItem(String item, boolean empty) { } else { setText(getString()); setGraphic(null); - - // Style based on enabled state + + // Style based on enabled state and add tooltip if (!row.isEnabled()) { setStyle("-fx-text-fill: grey;"); } else { setStyle(""); } + + // Add tooltip with parameter description + if (row.getDescription() != null && !row.getDescription().isEmpty()) { + Tooltip tooltip = new Tooltip(row.getDescription()); + tooltip.setWrapText(true); + tooltip.setMaxWidth(300); + setTooltip(tooltip); + } } } } - + @Override public void startEdit() { ParameterRow row = getTableRow().getItem(); @@ -93,27 +108,35 @@ public void startEdit() { textField.requestFocus(); } } - + @Override public void cancelEdit() { super.cancelEdit(); setText(getString()); setGraphic(null); } - + @Override public void commitEdit(String newValue) { super.commitEdit(newValue); ParameterRow row = getTableRow().getItem(); if (row != null) { row.setValue(newValue); - if (!isEditMode) { - isEditMode = true; - } + switchToEditingMode(); updateButtonStates(); + // Update cell color based on validation + updateValidationColor(row); } } - + + private void updateValidationColor(ParameterRow row) { + if (row.validate()) { + setStyle(""); + } else { + setStyle("-fx-text-fill: red;"); + } + } + private void createTextField() { textField = new TextField(getString()); textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2); @@ -124,11 +147,11 @@ private void createTextField() { } }); } - + private String getString() { return getItem() == null ? "" : getItem(); } - + // Make cell clickable to start editing { setOnMouseClicked(e -> { @@ -141,7 +164,7 @@ private String getString() { }); } } - + public static class ParameterRow { private final SimpleStringProperty name; private final SimpleBooleanProperty enabled; @@ -149,9 +172,9 @@ public static class ParameterRow { private final SimpleStringProperty description; private final SimpleBooleanProperty isOptional; private final Object defaultValue; - - public ParameterRow(String name, boolean enabled, String value, String description, - boolean isOptional, Object defaultValue) { + + public ParameterRow(String name, boolean enabled, String value, String description, + boolean isOptional, Object defaultValue) { this.name = new SimpleStringProperty(name); this.enabled = new SimpleBooleanProperty(enabled); this.value = new SimpleStringProperty(value != null ? value : ""); @@ -159,13 +182,13 @@ public ParameterRow(String name, boolean enabled, String value, String descripti this.isOptional = new SimpleBooleanProperty(isOptional); this.defaultValue = defaultValue; } - + public SimpleStringProperty nameProperty() { return name; } public SimpleBooleanProperty enabledProperty() { return enabled; } public SimpleStringProperty valueProperty() { return value; } public SimpleStringProperty descriptionProperty() { return description; } public SimpleBooleanProperty isOptionalProperty() { return isOptional; } - + public String getName() { return name.get(); } public boolean isEnabled() { return enabled.get(); } public void setEnabled(boolean enabled) { this.enabled.set(enabled); } @@ -174,41 +197,79 @@ public ParameterRow(String name, boolean enabled, String value, String descripti public String getDescription() { return description.get(); } public boolean isOptional() { return isOptional.get(); } public Object getDefaultValue() { return defaultValue; } - + public Object getParsedValue() { if (!enabled.get()) return defaultValue; - + String valueStr = value.get(); if (valueStr == null || valueStr.trim().isEmpty()) { return defaultValue; } - + try { - if (valueStr.equals("true") || valueStr.equals("false")) { - return Boolean.parseBoolean(valueStr); - } - if (valueStr.matches("-?\\d+")) { - return Integer.parseInt(valueStr); - } - if (valueStr.matches("-?\\d+\\.\\d+")) { - return Double.parseDouble(valueStr); - } - return valueStr; + return parseLiteralValue(valueStr); } catch (Exception e) { return valueStr; } } - + + private Object parseLiteralValue(String valueStr) throws Exception { + valueStr = valueStr.trim(); + + // Handle None/null + if ("None".equals(valueStr) || "null".equals(valueStr)) { + return null; + } + + // Handle booleans + if ("True".equals(valueStr) || "true".equals(valueStr)) { + return true; + } + if ("False".equals(valueStr) || "false".equals(valueStr)) { + return false; + } + + // Handle strings (quoted) + if ((valueStr.startsWith("'") && valueStr.endsWith("'")) || + (valueStr.startsWith("\"") && valueStr.endsWith("\""))) { + return valueStr.substring(1, valueStr.length() - 1); + } + + // Handle numbers + try { + if (valueStr.contains(".")) { + return Double.parseDouble(valueStr); + } else { + return Long.parseLong(valueStr); + } + } catch (NumberFormatException e) { + // Continue to other parsing attempts + } + + // Handle lists and dicts using JSON parsing (similar to Python's literal_eval) + if (valueStr.startsWith("[") || valueStr.startsWith("{")) { + try { + ObjectMapper mapper = new ObjectMapper(); + return mapper.readValue(valueStr, Object.class); + } catch (Exception e) { + // Fall back to string if JSON parsing fails + } + } + + // Default to string + return valueStr; + } + public boolean validate() { if (!enabled.get()) { - return isOptional.get(); + return true; // Disabled parameters are always valid } - + String valueStr = value.get(); if (valueStr == null || valueStr.trim().isEmpty()) { return isOptional.get(); } - + try { getParsedValue(); return true; @@ -216,47 +277,70 @@ public boolean validate() { return false; } } - + public boolean isEditable() { // Parameter is editable if it's enabled or if it's required (not optional) return enabled.get() || !isOptional.get(); } } - + @Override public void initialize(URL location, ResourceBundle resources) { initializeTable(); initializeControls(); loadAllowedPlansAndInstructions(); + + // Listen for edit requests from plan viewer + PlanEditEvent.getInstance().addListener(this::editItem); } - + private void initializeTable() { paramCol.setCellValueFactory(new PropertyValueFactory<>("name")); chkCol.setCellValueFactory(new PropertyValueFactory<>("enabled")); valueCol.setCellValueFactory(new PropertyValueFactory<>("value")); - + + // Add tooltips to parameter names + paramCol.setCellFactory(column -> { + TableCell cell = new TableCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || getTableRow() == null || getTableRow().getItem() == null) { + setText(null); + setTooltip(null); + } else { + setText(item); + ParameterRow row = getTableRow().getItem(); + if (row != null && row.getDescription() != null && !row.getDescription().isEmpty()) { + Tooltip tooltip = new Tooltip(row.getDescription()); + tooltip.setWrapText(true); + tooltip.setMaxWidth(300); + setTooltip(tooltip); + } + } + } + }; + return cell; + }); + chkCol.setCellFactory(CheckBoxTableCell.forTableColumn(chkCol)); chkCol.setEditable(true); - - // Custom editable cell that works like Python - click to edit immediately + valueCol.setCellFactory(column -> new EditableTableCell()); - + valueCol.setOnEditCommit(event -> { ParameterRow row = event.getRowValue(); row.setValue(event.getNewValue()); - // Trigger edit mode when value is changed (like Python) - if (!isEditMode) { - isEditMode = true; - } + switchToEditingMode(); updateButtonStates(); }); - + // Add listener for checkbox changes to enable/disable editing and trigger edit mode chkCol.setOnEditCommit(event -> { ParameterRow row = event.getRowValue(); boolean isChecked = event.getNewValue(); row.setEnabled(isChecked); - + // If unchecked, reset to default value; if checked and no value, set default if (!isChecked) { Object defaultValue = row.getDefaultValue(); @@ -264,75 +348,73 @@ private void initializeTable() { } else if (row.getValue().isEmpty() && row.getDefaultValue() != null) { row.setValue(String.valueOf(row.getDefaultValue())); } - - // Trigger edit mode when checkbox is changed (like Python code) - if (!isEditMode) { - isEditMode = true; - } - + + // Trigger edit mode when checkbox is changed + switchToEditingMode(); + updateButtonStates(); autoResizeColumns(); }); - + table.setItems(parameterRows); table.setEditable(true); } - + private void initializeControls() { ToggleGroup typeGroup = new ToggleGroup(); planRadBtn.setToggleGroup(typeGroup); instrRadBtn.setToggleGroup(typeGroup); planRadBtn.setSelected(true); - + // Setup ChoiceBox to auto-size to fit content exactly setupChoiceBoxAutoSizing(); - + choiceBox.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { if (newVal != null) { loadParametersForSelection(newVal); + // Set tooltip for choice box with item description + setChoiceBoxTooltip(newVal); } updateButtonStates(); // Resize ChoiceBox to fit new selection resizeChoiceBoxToFitContent(); }); - + planRadBtn.selectedProperty().addListener((obs, oldVal, newVal) -> { - if (newVal) { + if (newVal && !isEditMode) { populateChoiceBox(true); updateButtonStates(); } }); - + instrRadBtn.selectedProperty().addListener((obs, oldVal, newVal) -> { - if (newVal) { + if (newVal && !isEditMode) { populateChoiceBox(false); updateButtonStates(); } }); - + addBtn.setOnAction(e -> addItemToQueue()); batchBtn.setOnAction(e -> openBatchUpload()); saveBtn.setOnAction(e -> saveItem()); resetBtn.setOnAction(e -> resetParametersToDefaults()); cancelBtn.setOnAction(e -> cancelEdit()); - - // Don't use setVisible - use setDisable instead like Python code - + updateButtonStates(); } - + private void loadAllowedPlansAndInstructions() { new Thread(() -> { try { // Load plans from API - use raw Map instead of Envelope since response structure doesn't match Map responseMap = svc.plansAllowedRaw(); - + if (responseMap != null && Boolean.TRUE.equals(responseMap.get("success"))) { allowedPlans.clear(); if (responseMap.containsKey("plans_allowed")) { Map plansData = (Map) responseMap.get("plans_allowed"); - + // Convert each plan entry to Map for (Map.Entry entry : plansData.entrySet()) { String planName = entry.getKey(); @@ -345,47 +427,46 @@ private void loadAllowedPlansAndInstructions() { } else { LOG.log(Level.WARNING, "Plans response failed. Response: " + responseMap); } - - // Hardcode instructions like Python code - no instructions_allowed endpoint exists + allowedInstructions.clear(); Map queueStopInstr = new HashMap<>(); queueStopInstr.put("name", "queue_stop"); queueStopInstr.put("description", "Stop execution of the queue."); queueStopInstr.put("parameters", List.of()); allowedInstructions.put("queue_stop", queueStopInstr); - + Platform.runLater(() -> { populateChoiceBox(planRadBtn.isSelected()); }); - + } catch (Exception e) { LOG.log(Level.WARNING, "Failed to load plans", e); } }).start(); } - + private void populateChoiceBox(boolean isPlans) { choiceBox.getItems().clear(); parameterRows.clear(); - + if (isPlans) { choiceBox.getItems().addAll(allowedPlans.keySet()); } else { choiceBox.getItems().addAll(allowedInstructions.keySet()); } - + Collections.sort(choiceBox.getItems()); updateButtonStates(); // Resize ChoiceBox after populating resizeChoiceBoxToFitContent(); } - + private void setupChoiceBoxAutoSizing() { // Set minimum width choiceBox.setMinWidth(Region.USE_PREF_SIZE); choiceBox.setMaxWidth(Region.USE_PREF_SIZE); } - + private void resizeChoiceBoxToFitContent() { Platform.runLater(() -> { String selectedText = choiceBox.getSelectionModel().getSelectedItem(); @@ -394,10 +475,10 @@ private void resizeChoiceBoxToFitContent() { Text text = new Text(selectedText); // Use default font since ChoiceBox doesn't have getFont() double textWidth = text.getLayoutBounds().getWidth(); - + // Add padding for dropdown arrow and margins (about 30px) double newWidth = textWidth + 30; - + // Set the exact width needed choiceBox.setPrefWidth(newWidth); choiceBox.setMinWidth(newWidth); @@ -411,57 +492,131 @@ private void resizeChoiceBoxToFitContent() { } }); } - + private void loadParametersForSelection(String selectedName) { parameterRows.clear(); - + Map itemInfo; if (planRadBtn.isSelected()) { itemInfo = allowedPlans.get(selectedName); } else { itemInfo = allowedInstructions.get(selectedName); } - + if (itemInfo == null) return; - + List> parameters = (List>) itemInfo.get("parameters"); if (parameters == null) parameters = new ArrayList<>(); - + Map currentKwargs = new HashMap<>(); if (isEditMode && currentItem != null) { currentKwargs = currentItem.kwargs() != null ? currentItem.kwargs() : new HashMap<>(); + // Store original values for reset functionality + originalParameterValues.clear(); + originalParameterValues.putAll(currentKwargs); } - + for (Map paramInfo : parameters) { String paramName = (String) paramInfo.get("name"); String description = (String) paramInfo.get("description"); + if (description == null || description.isEmpty()) { + description = "Description for parameter '" + paramName + "' was not found..."; + } Object defaultValue = paramInfo.get("default"); - boolean isOptional = defaultValue != null; - + boolean isOptional = defaultValue != null || "VAR_POSITIONAL".equals(paramInfo.get("kind")) || + "VAR_KEYWORD".equals(paramInfo.get("kind")); + String currentValue = ""; boolean isEnabled = !isOptional; + if (currentKwargs.containsKey(paramName)) { - currentValue = String.valueOf(currentKwargs.get(paramName)); + Object value = currentKwargs.get(paramName); + currentValue = value != null ? String.valueOf(value) : ""; isEnabled = true; } else if (defaultValue != null) { currentValue = String.valueOf(defaultValue); + if (!isEditMode) { + currentValue += " (default)"; + } } - + ParameterRow row = new ParameterRow(paramName, isEnabled, currentValue, description, isOptional, defaultValue); parameterRows.add(row); } - + + if (isEditMode && currentItem != null && currentItem.result() != null) { + addMetadataAndResults(currentItem); + } + updateButtonStates(); autoResizeColumns(); } - + + private void addMetadataAndResults(QueueItem item) { + if (item.result() == null) { + return; + } + + Map result = item.result(); + + // Add a separator row for metadata section + if (!result.isEmpty()) { + ParameterRow separator = new ParameterRow("--- Metadata & Results ---", false, "", + "Execution metadata and results (read-only)", false, null); + parameterRows.add(separator); + } + + // Add metadata fields as read-only rows + for (Map.Entry entry : result.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + String displayValue = formatResultValue(value); + String description = "Result field: " + key + " (read-only)"; + + ParameterRow row = new ParameterRow(key, false, displayValue, description, false, null); + parameterRows.add(row); + } + } + + private String formatResultValue(Object value) { + if (value == null) { + return "null"; + } + + if (value instanceof Map) { + Map map = (Map) value; + if (map.isEmpty()) { + return "{}"; + } + return "Map (" + map.size() + " entries)"; + } + + if (value instanceof List) { + List list = (List) value; + if (list.isEmpty()) { + return "[]"; + } + return "List (" + list.size() + " items)"; + } + + if (value instanceof String) { + String str = (String) value; + if (str.length() > 100) { + return str.substring(0, 97) + "..."; + } + return str; + } + + return String.valueOf(value); + } + private void autoResizeColumns() { table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY); for (TableColumn col : table.getColumns()) { - + Text tmp = new Text(col.getText()); double max = tmp.getLayoutBounds().getWidth(); - + for (int i = 0; i < parameterRows.size(); i++) { Object cell = col.getCellData(i); if (cell != null) { @@ -473,72 +628,86 @@ private void autoResizeColumns() { col.setPrefWidth(max + 14); } } - + private boolean areParametersValid() { + boolean allValid = true; for (ParameterRow row : parameterRows) { - if (!row.validate()) { - return false; + boolean rowValid = row.validate(); + if (!rowValid) { + allValid = false; } + // Update visual validation state + updateRowValidationStyle(row, rowValid); } - return true; + return allValid; + } + + private void updateRowValidationStyle(ParameterRow row, boolean isValid) { + // This would need to be called to update the cell styling + // For now, we'll handle this in the cell factory + Platform.runLater(() -> { + table.refresh(); + }); } - + private void updateButtonStates() { boolean hasSelectedPlan = choiceBox.getSelectionModel().getSelectedItem() != null; boolean isValid = areParametersValid(); boolean isConnected = true; // Assume connected for now - could check StatusBus later - - // Add button: enabled when plan selected, valid parameters, connected, and not in edit mode - addBtn.setDisable(!hasSelectedPlan || !isValid || !isConnected || isEditMode); - - // Save button: enabled when in edit mode, plan selected, valid parameters, and connected - saveBtn.setDisable(!isEditMode || !hasSelectedPlan || !isValid || !isConnected); - - // Batch upload button: enabled when connected + this.editorStateValid = isValid; + + planRadBtn.setDisable(isEditMode); + instrRadBtn.setDisable(isEditMode); + choiceBox.setDisable(isEditMode); + + addBtn.setDisable(!hasSelectedPlan || !isValid || !isConnected); + + saveBtn.setDisable(!isEditMode || !hasSelectedPlan || !isValid || !isConnected || + !"QUEUE ITEM".equals(currentItemSource)); + batchBtn.setDisable(!isConnected); - - // Reset and Cancel buttons: enabled only in edit mode + resetBtn.setDisable(!isEditMode); cancelBtn.setDisable(!isEditMode); } - + private void addItemToQueue() { if (!areParametersValid()) { return; } - + try { String selectedName = choiceBox.getSelectionModel().getSelectedItem(); if (selectedName == null) { return; } - + String itemType = planRadBtn.isSelected() ? "plan" : "instruction"; Map kwargs = new HashMap<>(); - + for (ParameterRow row : parameterRows) { if (row.isEnabled()) { kwargs.put(row.getName(), row.getParsedValue()); } } - + QueueItem item = new QueueItem( - itemType, - selectedName, - List.of(), - kwargs, - null, - currentUser, - currentUserGroup, - null + itemType, + selectedName, + List.of(), + kwargs, + null, + currentUser, + currentUserGroup, + null ); - + QueueItemAdd request = new QueueItemAdd( - new QueueItemAdd.Item(item.itemType(), item.name(), item.args(), item.kwargs()), - currentUser, - currentUserGroup + new QueueItemAdd.Item(item.itemType(), item.name(), item.args(), item.kwargs()), + currentUser, + currentUserGroup ); - + new Thread(() -> { try { var response = svc.queueItemAdd(request); @@ -549,111 +718,155 @@ private void addItemToQueue() { choiceBox.getSelectionModel().clearSelection(); // Don't reset radio button - keep current selection populateChoiceBox(planRadBtn.isSelected()); + // Switch to view tab + TabSwitchEvent.getInstance().switchToTab("Plan Viewer"); + exitEditMode(); + showItemPreview(); } }); } catch (Exception e) { e.printStackTrace(); } }).start(); - + } catch (Exception e) { e.printStackTrace(); } } - + private void saveItem() { if (!isEditMode || currentItem == null) { return; } - + if (!areParametersValid()) { return; } - + try { String selectedName = choiceBox.getSelectionModel().getSelectedItem(); if (selectedName == null) { return; } - + String itemType = planRadBtn.isSelected() ? "plan" : "instruction"; Map kwargs = new HashMap<>(); - + for (ParameterRow row : parameterRows) { if (row.isEnabled()) { kwargs.put(row.getName(), row.getParsedValue()); } } - + QueueItem updatedItem = new QueueItem( - itemType, - selectedName, - List.of(), - kwargs, - currentItem.itemUid(), - currentItem.user(), - currentItem.userGroup(), - currentItem.result() + itemType, + selectedName, + List.of(), + kwargs, + currentItem.itemUid(), + currentItem.user(), + currentItem.userGroup(), + currentItem.result() ); - + new Thread(() -> { try { var response = svc.queueItemUpdate(updatedItem); Platform.runLater(() -> { if (response.success()) { + // Notify viewer that the item was updated + ItemUpdateEvent.getInstance().notifyItemUpdated(updatedItem); + + // Clear parameters but preserve radio button selection + parameterRows.clear(); + choiceBox.getSelectionModel().clearSelection(); + // Switch to view tab + TabSwitchEvent.getInstance().switchToTab("Plan Viewer"); exitEditMode(); + showItemPreview(); } else { - System.err.println("Save failed: " + response.msg()); + LOG.log(Level.WARNING, "Save failed: " + response.msg()); } }); } catch (Exception e) { - System.err.println("Save error: " + e.getMessage()); - e.printStackTrace(); + LOG.log(Level.WARNING, "Save error", e); } }).start(); - + } catch (Exception e) { e.printStackTrace(); } } - + private void resetForm() { parameterRows.clear(); choiceBox.getSelectionModel().clearSelection(); planRadBtn.setSelected(true); populateChoiceBox(true); } - + private void resetParametersToDefaults() { - // Reset all parameters to their default values (like Python reset functionality) - for (ParameterRow row : parameterRows) { - Object defaultValue = row.getDefaultValue(); - if (defaultValue != null) { - row.setValue(String.valueOf(defaultValue)); - } else { - row.setValue(""); + if (isEditMode && currentItem != null) { + // Reset to original values from when editing started + for (ParameterRow row : parameterRows) { + String paramName = row.getName(); + if (originalParameterValues.containsKey(paramName)) { + // Restore original value + Object originalValue = originalParameterValues.get(paramName); + row.setValue(originalValue != null ? String.valueOf(originalValue) : ""); + row.setEnabled(true); + } else { + // Parameter was not in original item, reset to default + Object defaultValue = row.getDefaultValue(); + if (defaultValue != null) { + row.setValue(String.valueOf(defaultValue)); + } else { + row.setValue(""); + } + row.setEnabled(!row.isOptional()); + } + } + } else { + // Reset to default values for new items + for (ParameterRow row : parameterRows) { + Object defaultValue = row.getDefaultValue(); + if (defaultValue != null) { + row.setValue(String.valueOf(defaultValue)); + } else { + row.setValue(""); + } + // Reset enabled state based on whether parameter is optional + row.setEnabled(!row.isOptional()); } - // Reset enabled state based on whether parameter is optional - row.setEnabled(!row.isOptional()); } // Keep edit mode active after reset - don't exit edit mode updateButtonStates(); autoResizeColumns(); } - + private void cancelEdit() { if (isEditMode) { - exitEditMode(); + // Reset to original state and exit edit mode + parameterRows.clear(); + isEditMode = false; + currentItem = null; + currentItemSource = ""; + originalParameterValues.clear(); + choiceBox.getSelectionModel().clearSelection(); + planRadBtn.setSelected(true); + populateChoiceBox(true); + updateButtonStates(); + showItemPreview(); } else { resetForm(); } - updateButtonStates(); } - + public void editItem(QueueItem item) { currentItem = item; + currentItemSource = "QUEUE ITEM"; isEditMode = true; - + if ("plan".equals(item.itemType())) { planRadBtn.setSelected(true); populateChoiceBox(true); @@ -661,48 +874,100 @@ public void editItem(QueueItem item) { instrRadBtn.setSelected(true); populateChoiceBox(false); } - + choiceBox.getSelectionModel().select(item.name()); - + + // Explicitly load parameters for the selected item since we're in edit mode + loadParametersForSelection(item.name()); + // Update button states instead of visibility updateButtonStates(); } - + + private void switchToEditingMode() { + if (!isEditMode) { + isEditMode = true; + currentItemSource = "NEW ITEM"; + updateButtonStates(); + } + } + private void exitEditMode() { isEditMode = false; currentItem = null; + currentItemSource = ""; + originalParameterValues.clear(); // Update button states instead of visibility updateButtonStates(); resetForm(); } - + private void openBatchUpload() { - // Basic file chooser for batch upload - matches Python queue_upload_spreadsheet + // Basic file chooser for batch upload javafx.stage.FileChooser fileChooser = new javafx.stage.FileChooser(); fileChooser.setTitle("Select Spreadsheet File"); fileChooser.getExtensionFilters().addAll( - new javafx.stage.FileChooser.ExtensionFilter("CSV Files", "*.csv"), - new javafx.stage.FileChooser.ExtensionFilter("Excel Files", "*.xlsx", "*.xls") + new javafx.stage.FileChooser.ExtensionFilter("CSV Files", "*.csv"), + new javafx.stage.FileChooser.ExtensionFilter("Excel Files", "*.xlsx", "*.xls") ); - + java.io.File file = fileChooser.showOpenDialog(table.getScene().getWindow()); if (file != null) { processBatchFile(file); } } - + private void processBatchFile(java.io.File file) { - // Basic batch processing - simplified version of Python's queue_upload_spreadsheet new Thread(() -> { try { - // For now, just show that batch upload was attempted - Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.INFORMATION); - alert.setTitle("Batch Upload"); - alert.setHeaderText(null); - alert.setContentText("Batch upload processing would happen here, similar to Python's queue_upload_spreadsheet method."); - alert.showAndWait(); - }); + List items = new ArrayList<>(); + + // Simple CSV parsing for now - in full implementation would handle Excel too + if (file.getName().toLowerCase().endsWith(".csv")) { + items = parseCSVFile(file); + } else if (file.getName().toLowerCase().endsWith(".xlsx") || + file.getName().toLowerCase().endsWith(".xls")) { + // Excel parsing would go here + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Batch Upload"); + alert.setHeaderText(null); + alert.setContentText("Excel file support not yet implemented. Use CSV files for now."); + alert.showAndWait(); + }); + return; + } + + if (!items.isEmpty()) { + final int itemCount = items.size(); + QueueItemAddBatch batchRequest = new QueueItemAddBatch(items, currentUser, currentUserGroup); + var response = svc.queueItemAddBatch(batchRequest); + + Platform.runLater(() -> { + if (response.success()) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Batch Upload Success"); + alert.setHeaderText(null); + alert.setContentText("Successfully added " + itemCount + " items to queue."); + alert.showAndWait(); + } else { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Batch Upload Failed"); + alert.setHeaderText(null); + alert.setContentText("Failed to add items to queue: " + response.msg()); + alert.showAndWait(); + } + }); + } else { + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("No Items Found"); + alert.setHeaderText(null); + alert.setContentText("No valid items found in the file."); + alert.showAndWait(); + }); + } + } catch (Exception e) { Platform.runLater(() -> { Alert alert = new Alert(Alert.AlertType.ERROR); @@ -714,5 +979,93 @@ private void processBatchFile(java.io.File file) { } }).start(); } - + + private List parseCSVFile(java.io.File file) throws Exception { + List items = new ArrayList<>(); + + try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(file))) { + String line = reader.readLine(); + if (line == null) return items; + + // Parse header + String[] headers = line.split(","); + + // Read data rows + while ((line = reader.readLine()) != null) { + String[] values = line.split(","); + if (values.length >= 2) { + String itemType = values[0].trim(); + String planName = values[1].trim(); + + Map kwargs = new HashMap<>(); + + // Parse additional parameters + for (int i = 2; i < Math.min(values.length, headers.length); i++) { + String paramName = headers[i].trim(); + String paramValue = values[i].trim(); + + if (!paramValue.isEmpty()) { + try { + // Try to parse as number or boolean + if (paramValue.equals("true") || paramValue.equals("false")) { + kwargs.put(paramName, Boolean.parseBoolean(paramValue)); + } else if (paramValue.matches("-?\\d+")) { + kwargs.put(paramName, Integer.parseInt(paramValue)); + } else if (paramValue.matches("-?\\d+\\.\\d+")) { + kwargs.put(paramName, Double.parseDouble(paramValue)); + } else { + kwargs.put(paramName, paramValue); + } + } catch (Exception e) { + kwargs.put(paramName, paramValue); + } + } + } + + QueueItem item = new QueueItem( + itemType.isEmpty() ? "plan" : itemType, + planName, + List.of(), + kwargs, + null, + currentUser, + currentUserGroup, + null + ); + items.add(item); + } + } + } + + return items; + } + + private void showItemPreview() { + String selectedItem = choiceBox.getSelectionModel().getSelectedItem(); + if (selectedItem != null) { + loadParametersForSelection(selectedItem); + } + } + + private void setChoiceBoxTooltip(String itemName) { + Map itemInfo; + if (planRadBtn.isSelected()) { + itemInfo = allowedPlans.get(itemName); + } else { + itemInfo = allowedInstructions.get(itemName); + } + + if (itemInfo != null) { + String description = (String) itemInfo.get("description"); + if (description != null && !description.isEmpty()) { + Tooltip tooltip = new Tooltip(description); + tooltip.setWrapText(true); + tooltip.setMaxWidth(400); + choiceBox.setTooltip(tooltip); + } else { + choiceBox.setTooltip(new Tooltip("Description for '" + itemName + "' was not found...")); + } + } + } + } \ No newline at end of file diff --git a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java index c10833639e..1d738c0727 100644 --- a/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java +++ b/app/queue-server/src/main/java/org/phoebus/applications/queueserver/controller/RePlanViewerController.java @@ -1,33 +1,468 @@ package org.phoebus.applications.queueserver.controller; +import org.phoebus.applications.queueserver.api.*; +import org.phoebus.applications.queueserver.client.RunEngineService; +import org.phoebus.applications.queueserver.util.QueueItemSelectionEvent; +import org.phoebus.applications.queueserver.view.PlanEditEvent; +import org.phoebus.applications.queueserver.view.TabSwitchEvent; +import org.phoebus.applications.queueserver.view.ItemUpdateEvent; +import javafx.application.Platform; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.fxml.FXML; -import javafx.scene.control.CheckBox; -import javafx.scene.control.Label; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -import javafx.scene.layout.HBox; +import javafx.fxml.Initializable; +import javafx.scene.control.*; +import javafx.scene.control.cell.CheckBoxTableCell; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.text.Text; -public class RePlanViewerController { +import java.net.URL; +import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; - @FXML - private TableColumn chkCol; +public class RePlanViewerController implements Initializable { - @FXML - private HBox editBtn; + @FXML private TableColumn paramCol; + @FXML private TableColumn chkCol; + @FXML private TableColumn valueCol; + @FXML private CheckBox paramChk; + @FXML private Label planLabel; + @FXML private TableView table; + @FXML private Button copyBtn, editBtn; - @FXML - private CheckBox paramChk; + private final RunEngineService svc = new RunEngineService(); + private static final Logger LOG = Logger.getLogger(RePlanViewerController.class.getName()); + private final ObservableList parameterRows = FXCollections.observableArrayList(); + private final Map> allowedPlans = new HashMap<>(); + private final Map> allowedInstructions = new HashMap<>(); - @FXML - private TableColumn paramCol; + private QueueItem currentQueueItem; + private String queueItemName = "-"; + private String queueItemType = null; + private boolean detailedView = true; // Show all parameters by default + private String currentUser = "GUI Client"; + private String currentUserGroup = "root"; - @FXML - private Label planLabel; + // Selection change handler - to be called from queue controller + private Runnable onEditItemRequested; - @FXML - private TableView table; + public static class ParameterRow { + private final SimpleStringProperty name; + private final SimpleBooleanProperty enabled; + private final SimpleStringProperty value; + private final SimpleStringProperty description; + private final SimpleBooleanProperty isOptional; + private final Object defaultValue; - @FXML - private TableColumn valueCol; + public ParameterRow(String name, boolean enabled, String value, String description, + boolean isOptional, Object defaultValue) { + this.name = new SimpleStringProperty(name); + this.enabled = new SimpleBooleanProperty(enabled); + this.value = new SimpleStringProperty(value != null ? value : ""); + this.description = new SimpleStringProperty(description != null ? description : ""); + this.isOptional = new SimpleBooleanProperty(isOptional); + this.defaultValue = defaultValue; + } -} + public SimpleStringProperty nameProperty() { return name; } + public SimpleBooleanProperty enabledProperty() { return enabled; } + public SimpleStringProperty valueProperty() { return value; } + public SimpleStringProperty descriptionProperty() { return description; } + public SimpleBooleanProperty isOptionalProperty() { return isOptional; } + + public String getName() { return name.get(); } + public boolean isEnabled() { return enabled.get(); } + public String getValue() { return value.get(); } + public String getDescription() { return description.get(); } + public boolean isOptional() { return isOptional.get(); } + public Object getDefaultValue() { return defaultValue; } + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + initializeTable(); + initializeControls(); + loadAllowedPlansAndInstructions(); + + QueueItemSelectionEvent.getInstance().addListener(this::showItem); + + ItemUpdateEvent.getInstance().addListener(this::handleItemUpdate); + } + + private void initializeTable() { + paramCol.setCellValueFactory(new PropertyValueFactory<>("name")); + chkCol.setCellValueFactory(new PropertyValueFactory<>("enabled")); + valueCol.setCellValueFactory(new PropertyValueFactory<>("value")); + + paramCol.setCellFactory(column -> { + TableCell cell = new TableCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || getTableRow() == null || getTableRow().getItem() == null) { + setText(null); + setTooltip(null); + } else { + setText(item); + ParameterRow row = getTableRow().getItem(); + if (row != null && row.getDescription() != null && !row.getDescription().isEmpty()) { + Tooltip tooltip = new Tooltip(row.getDescription()); + tooltip.setWrapText(true); + tooltip.setMaxWidth(300); + setTooltip(tooltip); + } + } + } + }; + return cell; + }); + + chkCol.setCellFactory(column -> { + CheckBoxTableCell cell = new CheckBoxTableCell<>(); + cell.setEditable(false); + return cell; + }); + + valueCol.setCellFactory(column -> { + TableCell cell = new TableCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || getTableRow() == null || getTableRow().getItem() == null) { + setText(null); + setTooltip(null); + } else { + setText(item); + ParameterRow row = getTableRow().getItem(); + if (!row.isEnabled()) { + setStyle("-fx-text-fill: grey;"); + } else { + setStyle(""); + } + + // Add tooltip with parameter description + if (row.getDescription() != null && !row.getDescription().isEmpty()) { + Tooltip tooltip = new Tooltip(row.getDescription()); + tooltip.setWrapText(true); + tooltip.setMaxWidth(300); + setTooltip(tooltip); + } + } + } + }; + return cell; + }); + + table.setItems(parameterRows); + table.setEditable(false); // Viewer is read-only + } + + private void initializeControls() { + paramChk.setSelected(detailedView); + paramChk.selectedProperty().addListener((obs, oldVal, newVal) -> { + detailedView = newVal; + if (currentQueueItem != null) { + showItem(currentQueueItem); + } + }); + + copyBtn.setOnAction(e -> copyToQueue()); + + editBtn.setOnAction(e -> editCurrentItem()); + + updateWidgetState(); + } + + private void loadAllowedPlansAndInstructions() { + new Thread(() -> { + try { + Map responseMap = svc.plansAllowedRaw(); + + if (responseMap != null && Boolean.TRUE.equals(responseMap.get("success"))) { + allowedPlans.clear(); + if (responseMap.containsKey("plans_allowed")) { + Map plansData = (Map) responseMap.get("plans_allowed"); + for (Map.Entry entry : plansData.entrySet()) { + String planName = entry.getKey(); + Map planInfo = (Map) entry.getValue(); + allowedPlans.put(planName, planInfo); + } + } + } + + allowedInstructions.clear(); + Map queueStopInstr = new HashMap<>(); + queueStopInstr.put("name", "queue_stop"); + queueStopInstr.put("description", "Stop execution of the queue."); + queueStopInstr.put("parameters", List.of()); + allowedInstructions.put("queue_stop", queueStopInstr); + + Platform.runLater(() -> updateWidgetState()); + + } catch (Exception e) { + LOG.log(Level.WARNING, "Failed to load plans", e); + } + }).start(); + } + + public void showItem(QueueItem item) { + currentQueueItem = item; + + String defaultName = "-"; + queueItemName = item != null ? (item.name() != null ? item.name() : defaultName) : defaultName; + queueItemType = item != null ? item.itemType() : null; + + String displayedItemType = "instruction".equals(queueItemType) ? "Instruction: " : "Plan: "; + planLabel.setText(displayedItemType + queueItemName); + + setItemDescriptionTooltip(); + + updateWidgetState(); + loadParametersForItem(item); + } + + private void setItemDescriptionTooltip() { + if (currentQueueItem != null) { + Map itemInfo; + if ("plan".equals(queueItemType)) { + itemInfo = allowedPlans.get(queueItemName); + } else { + itemInfo = allowedInstructions.get(queueItemName); + } + + if (itemInfo != null) { + String description = (String) itemInfo.get("description"); + if (description != null && !description.isEmpty()) { + Tooltip tooltip = new Tooltip(description); + tooltip.setWrapText(true); + tooltip.setMaxWidth(400); + planLabel.setTooltip(tooltip); + } else { + planLabel.setTooltip(new Tooltip("Description for '" + queueItemName + "' was not found...")); + } + } + } else { + planLabel.setTooltip(null); + } + } + + private void loadParametersForItem(QueueItem item) { + parameterRows.clear(); + + if (item == null) { + return; + } + + Map itemInfo; + if ("plan".equals(item.itemType())) { + itemInfo = allowedPlans.get(item.name()); + } else { + itemInfo = allowedInstructions.get(item.name()); + } + + if (itemInfo == null) return; + + List> parameters = (List>) itemInfo.get("parameters"); + if (parameters == null) parameters = new ArrayList<>(); + + Map itemKwargs = item.kwargs() != null ? item.kwargs() : new HashMap<>(); + + for (Map paramInfo : parameters) { + String paramName = (String) paramInfo.get("name"); + String description = (String) paramInfo.get("description"); + if (description == null || description.isEmpty()) { + description = "Description for parameter '" + paramName + "' was not found..."; + } + Object defaultValue = paramInfo.get("default"); + boolean isOptional = defaultValue != null || "VAR_POSITIONAL".equals(paramInfo.get("kind")) || + "VAR_KEYWORD".equals(paramInfo.get("kind")); + + String currentValue = ""; + boolean isEnabled = false; + + if (itemKwargs.containsKey(paramName)) { + Object value = itemKwargs.get(paramName); + currentValue = value != null ? String.valueOf(value) : ""; + isEnabled = true; + } else if (defaultValue != null) { + currentValue = String.valueOf(defaultValue) + " (default)"; + isEnabled = false; + } + + boolean shouldShow = detailedView || isEnabled; + + if (shouldShow) { + ParameterRow row = new ParameterRow(paramName, isEnabled, currentValue, description, isOptional, defaultValue); + parameterRows.add(row); + } + } + + addMetadataAndResults(item); + + autoResizeColumns(); + } + + private void addMetadataAndResults(QueueItem item) { + if (item.result() == null) { + return; + } + + Map result = item.result(); + + if (!result.isEmpty()) { + ParameterRow separator = new ParameterRow("--- Metadata & Results ---", false, "", + "Execution metadata and results", false, null); + parameterRows.add(separator); + } + + for (Map.Entry entry : result.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + String displayValue = formatResultValue(value); + String description = "Result field: " + key; + + ParameterRow row = new ParameterRow(key, true, displayValue, description, false, null); + parameterRows.add(row); + } + } + + private String formatResultValue(Object value) { + if (value == null) { + return "null"; + } + + if (value instanceof Map) { + Map map = (Map) value; + if (map.isEmpty()) { + return "{}"; + } + return "Map (" + map.size() + " entries)"; + } + + if (value instanceof List) { + List list = (List) value; + if (list.isEmpty()) { + return "[]"; + } + return "List (" + list.size() + " items)"; + } + + if (value instanceof String) { + String str = (String) value; + if (str.length() > 100) { + return str.substring(0, 97) + "..."; + } + return str; + } + + return String.valueOf(value); + } + + private void autoResizeColumns() { + table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY); + for (TableColumn col : table.getColumns()) { + Text tmp = new Text(col.getText()); + double max = tmp.getLayoutBounds().getWidth(); + + for (int i = 0; i < parameterRows.size(); i++) { + Object cell = col.getCellData(i); + if (cell != null) { + tmp = new Text(cell.toString()); + double w = tmp.getLayoutBounds().getWidth(); + if (w > max) max = w; + } + } + col.setPrefWidth(max + 14); + } + } + + private void updateWidgetState() { + boolean isItemAllowed = false; + boolean isConnected = true; // Assume connected for now + + if (queueItemType != null && queueItemName != null && !"-".equals(queueItemName)) { + if ("plan".equals(queueItemType)) { + isItemAllowed = allowedPlans.get(queueItemName) != null; + } else if ("instruction".equals(queueItemType)) { + isItemAllowed = allowedInstructions.get(queueItemName) != null; + } + } + + copyBtn.setDisable(!isItemAllowed || !isConnected); + + editBtn.setDisable(!isItemAllowed); + } + + private void copyToQueue() { + if (currentQueueItem == null) { + return; + } + + try { + // Create a copy of the current item for adding to queue + QueueItem itemCopy = new QueueItem( + currentQueueItem.itemType(), + currentQueueItem.name(), + currentQueueItem.args() != null ? currentQueueItem.args() : List.of(), + currentQueueItem.kwargs() != null ? currentQueueItem.kwargs() : new HashMap<>(), + null, // New item, no UID + currentUser, + currentUserGroup, + null // No result for new item + ); + + QueueItemAdd request = new QueueItemAdd( + new QueueItemAdd.Item(itemCopy.itemType(), itemCopy.name(), itemCopy.args(), itemCopy.kwargs()), + currentUser, + currentUserGroup + ); + + new Thread(() -> { + try { + var response = svc.queueItemAdd(request); + Platform.runLater(() -> { + if (!response.success()) { + LOG.log(Level.WARNING, "Copy to queue failed", response.msg()); + } + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Copy to queue error", e); + } + }).start(); + + } catch (Exception e) { + LOG.log(Level.WARNING, "Copy to queue error", e); + } + } + + private void editCurrentItem() { + if (currentQueueItem != null) { + PlanEditEvent.getInstance().notifyEditRequested(currentQueueItem); + TabSwitchEvent.getInstance().switchToTab("Plan Editor"); + } + } + + public void setOnEditItemRequested(Runnable callback) { + this.onEditItemRequested = callback; + } + + public QueueItem getCurrentItem() { + return currentQueueItem; + } + + private void handleItemUpdate(QueueItem updatedItem) { + // If this is the item currently being viewed, refresh the display + if (currentQueueItem != null && + updatedItem != null && + updatedItem.itemUid() != null && + updatedItem.itemUid().equals(currentQueueItem.itemUid())) { + + // Update the current item and refresh the display + Platform.runLater(() -> showItem(updatedItem)); + } + } + +} \ No newline at end of file From f3117af6ff4a9fceaef270125d02b71382840586 Mon Sep 17 00:00:00 2001 From: JMit-dev Date: Wed, 30 Jul 2025 10:11:03 -0400 Subject: [PATCH 7/8] fix: fxml name mismatch --- .../queueserver/view/RePlanEditor.fxml | 28 +++++++++---------- .../queueserver/view/RePlanViewer.fxml | 24 ++++++++-------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanEditor.fxml b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanEditor.fxml index f50641154c..80579e34e7 100644 --- a/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanEditor.fxml +++ b/app/queue-server/src/main/resources/org/phoebus/applications/queueserver/view/RePlanEditor.fxml @@ -19,23 +19,23 @@ - - - - - + + + + + -