diff --git a/app/queue-server/pom.xml b/app/queue-server/pom.xml
index 77e394bd1d..85ed682bee 100644
--- a/app/queue-server/pom.xml
+++ b/app/queue-server/pom.xml
@@ -34,5 +34,11 @@
jackson-dataformat-yaml
2.19.1
+
+ org.apache.poi
+ poi
+ 5.0.0
+ compile
+
-
+
\ No newline at end of file
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
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..587a979aa1 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,9 +1,22 @@
package org.phoebus.applications.queueserver.controller;
-import org.phoebus.applications.queueserver.api.QueueItem;
-import org.phoebus.applications.queueserver.api.QueueItemAdd;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.DateUtil;
+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.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import javafx.scene.layout.VBox;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.GridPane;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
@@ -35,7 +48,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 +58,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 +87,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 +118,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 +157,11 @@ private void createTextField() {
}
});
}
-
+
private String getString() {
return getItem() == null ? "" : getItem();
}
-
+
// Make cell clickable to start editing
{
setOnMouseClicked(e -> {
@@ -141,7 +174,7 @@ private String getString() {
});
}
}
-
+
public static class ParameterRow {
private final SimpleStringProperty name;
private final SimpleBooleanProperty enabled;
@@ -149,9 +182,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 +192,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 +207,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 +287,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 +358,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 +437,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 +485,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 +502,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