419 lines
18 KiB
Java
419 lines
18 KiB
Java
package de.opcua.app.controller;
|
|
|
|
import de.opcua.app.model.NodeAction;
|
|
import de.opcua.app.model.NodeRow;
|
|
import de.opcua.app.service.ActionService;
|
|
import javafx.geometry.Insets;
|
|
import javafx.geometry.Pos;
|
|
import javafx.scene.control.*;
|
|
import javafx.scene.layout.GridPane;
|
|
import javafx.scene.layout.HBox;
|
|
import javafx.scene.layout.Priority;
|
|
import javafx.scene.layout.VBox;
|
|
|
|
/**
|
|
* COMPLETELY FIXED: All fields are now fully editable and functional
|
|
*/
|
|
public class ActionButtonHelper {
|
|
|
|
private final ActionService actionService;
|
|
|
|
public ActionButtonHelper(ActionService actionService) {
|
|
this.actionService = actionService;
|
|
}
|
|
|
|
public TableColumn<NodeRow, Void> createActionColumn() {
|
|
TableColumn<NodeRow, Void> actionCol = new TableColumn<>("Actions");
|
|
actionCol.setPrefWidth(120);
|
|
|
|
actionCol.setCellFactory(param -> new TableCell<>() {
|
|
private final Button btnActions = new Button("Actions");
|
|
private final HBox container = new HBox(5);
|
|
|
|
{
|
|
btnActions.setMaxWidth(Double.MAX_VALUE);
|
|
container.getChildren().add(btnActions);
|
|
container.setAlignment(Pos.CENTER);
|
|
|
|
btnActions.setOnAction(event -> {
|
|
NodeRow row = getTableView().getItems().get(getIndex());
|
|
if (row != null && row.isSelected()) {
|
|
showActionDialog(row);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
protected void updateItem(Void item, boolean empty) {
|
|
super.updateItem(item, empty);
|
|
|
|
if (empty || getIndex() >= getTableView().getItems().size()) {
|
|
setGraphic(null);
|
|
} else {
|
|
NodeRow row = getTableView().getItems().get(getIndex());
|
|
if (row != null && row.isSelected() && row.isReadable()) {
|
|
setGraphic(container);
|
|
} else {
|
|
setGraphic(null);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return actionCol;
|
|
}
|
|
|
|
private void showActionDialog(NodeRow nodeRow) {
|
|
Dialog<ButtonType> dialog = new Dialog<>();
|
|
dialog.setTitle("Manage Actions");
|
|
dialog.setHeaderText("Actions for: " + nodeRow.getDisplayName() + "\nNode: " + nodeRow.getNodeId());
|
|
|
|
VBox mainBox = new VBox(10);
|
|
mainBox.setPadding(new Insets(10));
|
|
mainBox.setPrefWidth(700);
|
|
mainBox.setPrefHeight(400);
|
|
|
|
ListView<NodeAction> actionList = new ListView<>();
|
|
actionList.setPrefHeight(300);
|
|
actionList.getItems().addAll(actionService.getActions(nodeRow.getNodeId()));
|
|
VBox.setVgrow(actionList, Priority.ALWAYS);
|
|
|
|
actionList.setCellFactory(param -> new ListCell<>() {
|
|
@Override
|
|
protected void updateItem(NodeAction action, boolean empty) {
|
|
super.updateItem(action, empty);
|
|
if (empty || action == null) {
|
|
setGraphic(null);
|
|
} else {
|
|
HBox cell = new HBox(10);
|
|
cell.setAlignment(Pos.CENTER_LEFT);
|
|
cell.setPadding(new Insets(5));
|
|
|
|
CheckBox enabled = new CheckBox();
|
|
enabled.setSelected(action.isEnabled());
|
|
enabled.setOnAction(e -> actionService.setActionEnabled(
|
|
action.getNodeId(),
|
|
action.getActionName(),
|
|
enabled.isSelected()
|
|
));
|
|
|
|
Label label = new Label(action.getActionName() + " (" + action.getTriggerType() + ")");
|
|
label.setMinWidth(250);
|
|
HBox.setHgrow(label, Priority.ALWAYS);
|
|
|
|
Button btnEdit = new Button("Edit");
|
|
btnEdit.setOnAction(e -> {
|
|
editAction(action);
|
|
actionList.refresh();
|
|
});
|
|
|
|
Button btnTest = new Button("Test");
|
|
btnTest.setOnAction(e -> testAction(action, nodeRow));
|
|
|
|
Button btnDelete = new Button("Delete");
|
|
btnDelete.setOnAction(e -> {
|
|
actionService.removeAction(action.getNodeId(), action.getActionName());
|
|
actionList.getItems().remove(action);
|
|
});
|
|
|
|
cell.getChildren().addAll(enabled, label, btnEdit, btnTest, btnDelete);
|
|
setGraphic(cell);
|
|
}
|
|
}
|
|
});
|
|
|
|
HBox buttonBox = new HBox(10);
|
|
buttonBox.setAlignment(Pos.CENTER_LEFT);
|
|
|
|
Button addButton = new Button("Add New Action");
|
|
addButton.setOnAction(e -> {
|
|
NodeAction newAction = createNewAction(nodeRow);
|
|
if (newAction != null) {
|
|
actionService.addAction(newAction);
|
|
actionList.getItems().add(newAction);
|
|
}
|
|
});
|
|
|
|
buttonBox.getChildren().add(addButton);
|
|
|
|
mainBox.getChildren().addAll(actionList, buttonBox);
|
|
|
|
dialog.getDialogPane().setContent(mainBox);
|
|
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.CLOSE);
|
|
dialog.setResizable(true);
|
|
dialog.showAndWait();
|
|
}
|
|
|
|
private NodeAction createNewAction(NodeRow nodeRow) {
|
|
Dialog<NodeAction> dialog = new Dialog<>();
|
|
dialog.setTitle("Create New Action");
|
|
dialog.setHeaderText("Add action for: " + nodeRow.getDisplayName());
|
|
dialog.setResizable(true);
|
|
|
|
// Main container
|
|
VBox mainContainer = new VBox(15);
|
|
mainContainer.setPadding(new Insets(20));
|
|
mainContainer.setPrefWidth(600);
|
|
|
|
// Name field
|
|
Label nameLabel = new Label("Action Name:");
|
|
nameLabel.setStyle("-fx-font-weight: bold;");
|
|
TextField nameField = new TextField();
|
|
nameField.setPromptText("e.g., Temperature Alarm");
|
|
nameField.setEditable(true); // ✅ EXPLICITLY EDITABLE
|
|
nameField.setDisable(false); // ✅ NOT DISABLED
|
|
|
|
// Trigger type
|
|
Label triggerLabel = new Label("Trigger Type:");
|
|
triggerLabel.setStyle("-fx-font-weight: bold;");
|
|
ComboBox<NodeAction.TriggerType> triggerCombo = new ComboBox<>();
|
|
triggerCombo.getItems().addAll(NodeAction.TriggerType.values());
|
|
triggerCombo.setValue(NodeAction.TriggerType.ON_CHANGE);
|
|
triggerCombo.setMaxWidth(Double.MAX_VALUE);
|
|
triggerCombo.setEditable(false); // ✅ ComboBox should not be text-editable
|
|
triggerCombo.setDisable(false); // ✅ But should be selectable
|
|
|
|
// Trigger value
|
|
Label valueLabel = new Label("Trigger Value (for ON_VALUE, ON_GREATER_THAN, ON_LESS_THAN):");
|
|
TextField triggerValueField = new TextField();
|
|
triggerValueField.setPromptText("e.g., 100");
|
|
triggerValueField.setEditable(true);
|
|
triggerValueField.setDisable(true); // Initially disabled
|
|
|
|
// Interval
|
|
Label intervalLabel = new Label("Interval in milliseconds (for ON_INTERVAL):");
|
|
TextField intervalField = new TextField("1000");
|
|
intervalField.setPromptText("e.g., 5000");
|
|
intervalField.setEditable(true);
|
|
intervalField.setDisable(true); // Initially disabled
|
|
|
|
// Script area
|
|
Label scriptLabel = new Label("JavaScript Code:");
|
|
scriptLabel.setStyle("-fx-font-weight: bold;");
|
|
TextArea scriptArea = new TextArea();
|
|
scriptArea.setPromptText(
|
|
"Example JavaScript:\n\n" +
|
|
"console.log('Value changed:', currentValue);\n" +
|
|
"console.log('Node:', nodeId);\n\n" +
|
|
"// Read from OPC UA\n" +
|
|
"var temp = opc.read('ns=2;s=Temperature');\n\n" +
|
|
"// Write to OPC UA\n" +
|
|
"opc.write('ns=2;s=Output', '123');\n\n" +
|
|
"// REST API call\n" +
|
|
"var response = rest.post(\n" +
|
|
" 'https://api.example.com/alert',\n" +
|
|
" JSON.stringify({value: currentValue})\n" +
|
|
");\n\n" +
|
|
"// Store data\n" +
|
|
"store.set('lastValue', currentValue);"
|
|
);
|
|
scriptArea.setPrefRowCount(12);
|
|
scriptArea.setWrapText(true);
|
|
scriptArea.setEditable(true); // ✅ EXPLICITLY EDITABLE
|
|
scriptArea.setDisable(false); // ✅ NOT DISABLED
|
|
VBox.setVgrow(scriptArea, Priority.ALWAYS);
|
|
|
|
// Dynamic field enabling
|
|
triggerCombo.valueProperty().addListener((obs, oldVal, newVal) -> {
|
|
if (newVal == null) return;
|
|
|
|
boolean needsValue = newVal == NodeAction.TriggerType.ON_VALUE ||
|
|
newVal == NodeAction.TriggerType.ON_GREATER_THAN ||
|
|
newVal == NodeAction.TriggerType.ON_LESS_THAN;
|
|
triggerValueField.setDisable(!needsValue);
|
|
|
|
boolean needsInterval = newVal == NodeAction.TriggerType.ON_INTERVAL;
|
|
intervalField.setDisable(!needsInterval);
|
|
|
|
// Update script prompt based on trigger
|
|
updateScriptPrompt(scriptArea, newVal);
|
|
});
|
|
|
|
// Add all to container
|
|
mainContainer.getChildren().addAll(
|
|
nameLabel, nameField,
|
|
triggerLabel, triggerCombo,
|
|
valueLabel, triggerValueField,
|
|
intervalLabel, intervalField,
|
|
scriptLabel, scriptArea
|
|
);
|
|
|
|
dialog.getDialogPane().setContent(mainContainer);
|
|
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
|
|
|
// Validation and conversion
|
|
dialog.setResultConverter(buttonType -> {
|
|
if (buttonType == ButtonType.OK) {
|
|
String name = nameField.getText();
|
|
if (name == null || name.trim().isEmpty()) {
|
|
showAlert("Validation Error", "Name is required", "Please enter a name for the action.");
|
|
return null;
|
|
}
|
|
|
|
String script = scriptArea.getText();
|
|
if (script == null || script.trim().isEmpty()) {
|
|
showAlert("Validation Error", "Script is required", "Please enter JavaScript code.");
|
|
return null;
|
|
}
|
|
|
|
NodeAction action = new NodeAction();
|
|
action.setNodeId(nodeRow.getNodeId());
|
|
action.setActionName(name.trim());
|
|
action.setTriggerType(triggerCombo.getValue());
|
|
action.setScript(script);
|
|
action.setTriggerValue(triggerValueField.getText());
|
|
|
|
try {
|
|
action.setIntervalMs(Integer.parseInt(intervalField.getText()));
|
|
} catch (NumberFormatException ex) {
|
|
action.setIntervalMs(1000);
|
|
}
|
|
|
|
return action;
|
|
}
|
|
return null;
|
|
});
|
|
|
|
// Set initial focus
|
|
javafx.application.Platform.runLater(() -> nameField.requestFocus());
|
|
|
|
return dialog.showAndWait().orElse(null);
|
|
}
|
|
|
|
private void editAction(NodeAction action) {
|
|
Dialog<ButtonType> dialog = new Dialog<>();
|
|
dialog.setTitle("Edit Action");
|
|
dialog.setHeaderText("Edit: " + action.getActionName());
|
|
dialog.setResizable(true);
|
|
|
|
VBox mainContainer = new VBox(15);
|
|
mainContainer.setPadding(new Insets(20));
|
|
mainContainer.setPrefWidth(600);
|
|
|
|
Label nameLabel = new Label("Action Name:");
|
|
nameLabel.setStyle("-fx-font-weight: bold;");
|
|
TextField nameField = new TextField(action.getActionName());
|
|
nameField.setEditable(true);
|
|
nameField.setDisable(false);
|
|
|
|
Label triggerLabel = new Label("Trigger Type:");
|
|
triggerLabel.setStyle("-fx-font-weight: bold;");
|
|
ComboBox<NodeAction.TriggerType> triggerCombo = new ComboBox<>();
|
|
triggerCombo.getItems().addAll(NodeAction.TriggerType.values());
|
|
triggerCombo.setValue(action.getTriggerType());
|
|
triggerCombo.setMaxWidth(Double.MAX_VALUE);
|
|
triggerCombo.setDisable(false);
|
|
|
|
Label valueLabel = new Label("Trigger Value:");
|
|
TextField triggerValueField = new TextField(action.getTriggerValue() != null ? action.getTriggerValue() : "");
|
|
triggerValueField.setEditable(true);
|
|
|
|
Label intervalLabel = new Label("Interval (ms):");
|
|
TextField intervalField = new TextField(String.valueOf(action.getIntervalMs()));
|
|
intervalField.setEditable(true);
|
|
|
|
Label scriptLabel = new Label("JavaScript Code:");
|
|
scriptLabel.setStyle("-fx-font-weight: bold;");
|
|
TextArea scriptArea = new TextArea(action.getScript());
|
|
scriptArea.setPrefRowCount(12);
|
|
scriptArea.setWrapText(true);
|
|
scriptArea.setEditable(true);
|
|
scriptArea.setDisable(false);
|
|
VBox.setVgrow(scriptArea, Priority.ALWAYS);
|
|
|
|
// Dynamic enabling
|
|
triggerCombo.valueProperty().addListener((obs, oldVal, newVal) -> {
|
|
if (newVal == null) return;
|
|
|
|
boolean needsValue = newVal == NodeAction.TriggerType.ON_VALUE ||
|
|
newVal == NodeAction.TriggerType.ON_GREATER_THAN ||
|
|
newVal == NodeAction.TriggerType.ON_LESS_THAN;
|
|
triggerValueField.setDisable(!needsValue);
|
|
|
|
boolean needsInterval = newVal == NodeAction.TriggerType.ON_INTERVAL;
|
|
intervalField.setDisable(!needsInterval);
|
|
});
|
|
|
|
// Set initial state
|
|
NodeAction.TriggerType type = action.getTriggerType();
|
|
boolean needsValue = type == NodeAction.TriggerType.ON_VALUE ||
|
|
type == NodeAction.TriggerType.ON_GREATER_THAN ||
|
|
type == NodeAction.TriggerType.ON_LESS_THAN;
|
|
triggerValueField.setDisable(!needsValue);
|
|
|
|
boolean needsInterval = type == NodeAction.TriggerType.ON_INTERVAL;
|
|
intervalField.setDisable(!needsInterval);
|
|
|
|
mainContainer.getChildren().addAll(
|
|
nameLabel, nameField,
|
|
triggerLabel, triggerCombo,
|
|
valueLabel, triggerValueField,
|
|
intervalLabel, intervalField,
|
|
scriptLabel, scriptArea
|
|
);
|
|
|
|
dialog.getDialogPane().setContent(mainContainer);
|
|
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
|
|
|
dialog.setResultConverter(buttonType -> {
|
|
if (buttonType == ButtonType.OK) {
|
|
action.setActionName(nameField.getText());
|
|
action.setTriggerType(triggerCombo.getValue());
|
|
action.setScript(scriptArea.getText());
|
|
action.setTriggerValue(triggerValueField.getText());
|
|
|
|
try {
|
|
action.setIntervalMs(Integer.parseInt(intervalField.getText()));
|
|
} catch (NumberFormatException ex) {
|
|
// Keep existing
|
|
}
|
|
}
|
|
return buttonType;
|
|
});
|
|
|
|
dialog.showAndWait();
|
|
}
|
|
|
|
private void testAction(NodeAction action, NodeRow nodeRow) {
|
|
String currentValue = nodeRow.getValue();
|
|
if (currentValue == null || currentValue.isEmpty()) {
|
|
currentValue = "test-value";
|
|
}
|
|
|
|
actionService.testAction(action.getNodeId(), action.getActionName(), currentValue);
|
|
|
|
showAlert(
|
|
"Action Test",
|
|
"Testing: " + action.getActionName(),
|
|
"Action triggered with value: " + currentValue + "\n\n" +
|
|
"Check the log area below for script output (look for [JS] prefix)."
|
|
);
|
|
}
|
|
|
|
private void updateScriptPrompt(TextArea scriptArea, NodeAction.TriggerType type) {
|
|
String prompt = switch (type) {
|
|
case ON_CHANGE -> "Triggered on any value change\n\nconsole.log('Value changed to:', currentValue);";
|
|
case ON_EVEN -> "Triggered when value is even\n\nconsole.log('Even value:', currentValue);";
|
|
case ON_ODD -> "Triggered when value is odd\n\nconsole.log('Odd value:', currentValue);";
|
|
case ON_GREATER_THAN -> "Triggered when value > threshold\n\nconsole.log('ALARM! Value too high:', currentValue);";
|
|
case ON_LESS_THAN -> "Triggered when value < threshold\n\nconsole.log('WARNING! Value too low:', currentValue);";
|
|
case ON_VALUE -> "Triggered when value equals specific value\n\nconsole.log('Target value reached:', currentValue);";
|
|
case ON_TRUE -> "Triggered when value becomes truthy\n\nconsole.log('Activated:', currentValue);";
|
|
case ON_FALSE -> "Triggered when value becomes falsy\n\nconsole.log('Deactivated:', currentValue);";
|
|
case ON_INTERVAL -> "Triggered periodically\n\nvar value = opc.read(nodeId);\nconsole.log('Periodic check:', value);";
|
|
};
|
|
|
|
if (scriptArea.getText().isEmpty()) {
|
|
scriptArea.setPromptText(prompt);
|
|
}
|
|
}
|
|
|
|
private void showAlert(String title, String header, String content) {
|
|
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
|
alert.setTitle(title);
|
|
alert.setHeaderText(header);
|
|
alert.setContentText(content);
|
|
alert.showAndWait();
|
|
}
|
|
}
|