From d02d019c3d0a9e3eef757fff1badaf1360e399dc Mon Sep 17 00:00:00 2001 From: Evan Daykin Date: Mon, 22 Apr 2024 12:45:47 -0400 Subject: [PATCH 01/20] skeleton for UXA plugin --- app/pom.xml | 1 + app/ux-analytics/pom.xml | 25 ++++++++ app/ux-analytics/ui/build.xml | 10 +++ app/ux-analytics/ui/pom.xml | 58 +++++++++++++++++ .../uxanalytics/ui/CreateUXAMenuEntry.java | 23 +++++++ .../uxanalytics/ui/UXAnalyticsUI.java | 62 +++++++++++++++++++ .../org.phoebus.framework.spi.AppDescriptor | 1 + .../services/org.phoebus.ui.spi.MenuEntry | 1 + .../uxanalytics/ui/uxa-settings-dialog.fxml | 14 +++++ 9 files changed, 195 insertions(+) create mode 100644 app/ux-analytics/pom.xml create mode 100644 app/ux-analytics/ui/build.xml create mode 100644 app/ux-analytics/ui/pom.xml create mode 100644 app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/CreateUXAMenuEntry.java create mode 100644 app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java create mode 100644 app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor create mode 100644 app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry create mode 100644 app/ux-analytics/ui/src/main/resources/org/phoebus/applications/uxanalytics/ui/uxa-settings-dialog.fxml diff --git a/app/pom.xml b/app/pom.xml index ff5bb5eee0..40d46dddca 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -36,5 +36,6 @@ imageviewer credentials-management eslog + ux-analytics diff --git a/app/ux-analytics/pom.xml b/app/ux-analytics/pom.xml new file mode 100644 index 0000000000..3894a3ce0e --- /dev/null +++ b/app/ux-analytics/pom.xml @@ -0,0 +1,25 @@ + + + 4.0.0 + 4.7.2-SNAPSHOT + app-ux-analytics + pom + + org.phoebus + app + 4.7.2-SNAPSHOT + + + + ui + + + + 22 + 22 + UTF-8 + + + \ No newline at end of file diff --git a/app/ux-analytics/ui/build.xml b/app/ux-analytics/ui/build.xml new file mode 100644 index 0000000000..fad90485ad --- /dev/null +++ b/app/ux-analytics/ui/build.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/ux-analytics/ui/pom.xml b/app/ux-analytics/ui/pom.xml new file mode 100644 index 0000000000..223c462867 --- /dev/null +++ b/app/ux-analytics/ui/pom.xml @@ -0,0 +1,58 @@ + + + + + org.phoebus + app-ux-analytics + 4.7.2-SNAPSHOT + + 4.0.0 + app-analytics-ui + + + 22 + 22 + UTF-8 + + + + org.openjfx + javafx-graphics + 19 + compile + + + org.openjfx + javafx-fxml + 19 + compile + + + org.phoebus + core-framework + 4.7.2-SNAPSHOT + compile + + + org.phoebus + core-ui + 4.7.2-SNAPSHOT + compile + + + + + + src/main/resources + + + src/main/java + + **/*.fxml + + + + + \ No newline at end of file diff --git a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/CreateUXAMenuEntry.java b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/CreateUXAMenuEntry.java new file mode 100644 index 0000000000..6d2c7802a3 --- /dev/null +++ b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/CreateUXAMenuEntry.java @@ -0,0 +1,23 @@ +package org.phoebus.applications.uxanalytics.ui; + +import org.phoebus.framework.workbench.ApplicationService; + +import org.phoebus.ui.spi.MenuEntry; + +public class CreateUXAMenuEntry implements MenuEntry { + + @Override + public String getName() { return UXAnalyticsUI.DISPLAY_NAME;} + + @Override + public Void call() throws Exception { + ApplicationService.createInstance(UXAnalyticsUI.NAME); + return null; + } + + @Override + public String getMenuPath() { + return "Utility"; + } + +} \ No newline at end of file diff --git a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java new file mode 100644 index 0000000000..1b006cf6b2 --- /dev/null +++ b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java @@ -0,0 +1,62 @@ +package org.phoebus.applications.uxanalytics.ui; + +import java.io.IOException; +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.phoebus.framework.spi.AppInstance; +import org.phoebus.framework.spi.AppResourceDescriptor; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +/** + * @author Evan Daykin + */ + +public class UXAnalyticsUI implements AppResourceDescriptor { + public static final Logger logger = Logger.getLogger(UXAnalyticsUI.class.getPackageName()); + public static final String NAME = "uxanalyticsconfig"; + public static final String DISPLAY_NAME = "UX Analytics Config"; + + @Override + public AppInstance create(URI resource) { + return create(); + } + + @Override + public String getName() { + return NAME; + } + + @Override + public String getDisplayName() { + return DISPLAY_NAME; + } + + @Override + public void start(){ + System.out.println("Starting my thing"); + } + + @Override + public AppInstance create() { + try{ + final FXMLLoader loader = new FXMLLoader(); + loader.setLocation(UXAnalyticsUI.class.getResource("uxa-settings-dialog.fxml")); + Parent root = loader.load(); + Scene scene = new Scene(root, 600, 800); + Stage stage = new Stage(); + stage.setTitle("Test"); + stage.setScene(scene); + stage.show(); + } + catch (IOException e) { + logger.log(Level.WARNING, "Failed to create UX Analytics dialog", e); + } + return null; + } +} \ No newline at end of file diff --git a/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor b/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor new file mode 100644 index 0000000000..aec13ca465 --- /dev/null +++ b/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor @@ -0,0 +1 @@ +org.phoebus.applications.uxanalytics.ui.UXAnalyticsUI diff --git a/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry b/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry new file mode 100644 index 0000000000..fc5b2423c7 --- /dev/null +++ b/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.ui.spi.MenuEntry @@ -0,0 +1 @@ +org.phoebus.applications.uxanalytics.ui.CreateUXAMenuEntry diff --git a/app/ux-analytics/ui/src/main/resources/org/phoebus/applications/uxanalytics/ui/uxa-settings-dialog.fxml b/app/ux-analytics/ui/src/main/resources/org/phoebus/applications/uxanalytics/ui/uxa-settings-dialog.fxml new file mode 100644 index 0000000000..fe27b2eebe --- /dev/null +++ b/app/ux-analytics/ui/src/main/resources/org/phoebus/applications/uxanalytics/ui/uxa-settings-dialog.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + From 865f5458fdbfda8eca769f08c385ec4c1b6e4cc4 Mon Sep 17 00:00:00 2001 From: Evan Daykin Date: Thu, 23 May 2024 13:24:32 -0400 Subject: [PATCH 02/20] Working ToolkitListener for tab containers --- .../runtime/app/DisplayRuntimeInstance.java | 9 +- .../monitor/ActiveTabsService.java | 73 +++++++++ .../monitor/ActiveWidgetsService.java | 35 +++++ .../monitor/ActiveWindowsService.java | 141 ++++++++++++++++++ .../monitor/GraphDatabaseToolkitListener.java | 23 +++ .../uxanalytics/monitor/UXAMonitor.java | 38 +++++ .../uxanalytics/monitor/UXWidgetWrapper.java | 17 +++ .../uxanalytics/ui/UXAController.java | 81 ++++++++++ .../uxanalytics/ui/UXAnalyticsUI.java | 17 ++- 9 files changed, 426 insertions(+), 8 deletions(-) create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXWidgetWrapper.java create mode 100644 app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAController.java diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java index 599a44c907..e59368e215 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java @@ -525,13 +525,14 @@ public void onClosed() navigation.dispose(); } - public void addListener(ToolkitListener listener){ - this.getRepresentation().removeListener(listener); - this.getRepresentation().addListener(listener); + public DisplayModel getActiveModel() + { + return active_model; } - public void removeListener(ToolkitListener listener){ + public void addListener(ToolkitListener listener){ this.getRepresentation().removeListener(listener); + this.getRepresentation().addListener(listener); } @Override diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java new file mode 100644 index 0000000000..19458d8e76 --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java @@ -0,0 +1,73 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import javafx.collections.*; +import javafx.stage.Window; +import org.csstudio.display.builder.model.DisplayModel; +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.widgets.EmbeddedDisplayWidget; +import org.csstudio.display.builder.representation.ToolkitRepresentation; +import org.csstudio.display.builder.representation.javafx.JFXRepresentation; +import org.csstudio.display.builder.runtime.app.DisplayRuntimeInstance; +import org.csstudio.display.builder.runtime.app.DockItemRepresentation; +import org.phoebus.ui.docking.DockItemWithInput; + +import java.util.ArrayList; + +public class ActiveTabsService { + + private final ActiveWindowsService activeWindowsService; + private final Window window; + private final ObservableMap activeTabs = FXCollections.observableHashMap(); + + public ActiveTabsService(Window window){ + this.window = window; + activeWindowsService = ActiveWindowsService.getInstance(); + } + + public void add(DockItemWithInput tab) throws Exception { + if (!activeTabs.containsKey(tab)){ + activeTabs.put(tab, new ActiveWidgetsService(tab)); + addAllWidgetsIn(tab); + } + } + + public void remove(DockItemWithInput tab){ + activeTabs.remove(tab); + } + + public boolean contains(DockItemWithInput tab){ + return activeTabs.containsKey(tab); + } + + public void addWidget(DockItemWithInput tab, Widget widget){ + if(activeTabs.containsKey(tab)){ + activeTabs.get(tab).add(widget); + } + } + + public void addAllWidgetsIn(DockItemWithInput tab) throws Exception { + DisplayRuntimeInstance instance = (DisplayRuntimeInstance) tab.getProperties().get("application"); + for(Widget widget: instance.getActiveModel().getChildren()){ + if(widget instanceof EmbeddedDisplayWidget){ + addAllWidgetsIn((EmbeddedDisplayWidget) widget, tab); + } + else{ + addWidget(tab, widget); + } + } + } + + public void addAllWidgetsIn(EmbeddedDisplayWidget widget, DockItemWithInput parentTab) throws Exception { + DisplayModel model = (DisplayModel) widget.getProperty("embedded_model").getValue(); + for(Widget embeddedWidget: model.getChildren()){ + if(embeddedWidget instanceof EmbeddedDisplayWidget){ + //recursively add widgets (We don't care that it's embedded, only that it's seen in this tab) + addAllWidgetsIn((EmbeddedDisplayWidget) embeddedWidget, parentTab); + } + else{ + addWidget(parentTab, embeddedWidget); + } + } + + } +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java new file mode 100644 index 0000000000..1cdacf241b --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java @@ -0,0 +1,35 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.representation.ToolkitListener; +import org.csstudio.display.builder.runtime.app.DisplayRuntimeInstance; +import org.phoebus.ui.docking.DockItemWithInput; + +import java.util.ArrayList; + +public class ActiveWidgetsService { + + private final ArrayList widgets; + private final DockItemWithInput parentTab; + private final ToolkitListener listener; + + public ActiveWidgetsService(DockItemWithInput tab){ + widgets = new ArrayList<>(); + parentTab = tab; + listener = new GraphDatabaseToolkitListener(); + ((DisplayRuntimeInstance)tab.getProperties().get("application")).addListener(listener); + } + + public ToolkitListener getListener(){ + return listener; + } + + public void add(Widget widget){ + widgets.add(widget); + } + + public void remove(Widget widget){ + widgets.remove(widget); + } + +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java new file mode 100644 index 0000000000..9335fed152 --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java @@ -0,0 +1,141 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import com.sun.javafx.collections.UnmodifiableObservableMap; + +import javafx.collections.*; +import javafx.scene.control.Tab; +import javafx.stage.Stage; +import javafx.stage.Window; + +import org.csstudio.display.builder.runtime.app.DisplayRuntimeInstance; +import org.phoebus.ui.docking.DockItemWithInput; +import org.phoebus.ui.docking.DockPane; +import org.phoebus.ui.docking.DockStage; + +import java.util.concurrent.ExecutionException; + + +/* + * Get active @Window instances + * For each window, add it to a map with a set of tabs as the value, + * Attach our custom ToolkitListener to each writable Widget. + * @author Evan Daykin + */ +public class ActiveWindowsService { + + private boolean started = false; + private static ActiveWindowsService instance = null; + private final ObservableMap activeWindowsAndTabs = FXCollections.observableHashMap(); + + //don't want anyone faffing about with the map, but they should be able to see it + public UnmodifiableObservableMap getActiveWindowsAndTabs() { + return new UnmodifiableObservableMap<>(activeWindowsAndTabs); + } + + ListChangeListener UXATabChangeListener = new ListChangeListener<>() { + @Override + public void onChanged(Change change) { + while(change.next()){ + if(change.wasAdded()){ + for(Tab tab: change.getAddedSubList()){ + Window window = change.getList().get(0).getTabPane().getScene().getWindow(); + if(tab.getProperties().get("application") instanceof DisplayRuntimeInstance){ + DisplayRuntimeInstance instance = ((DisplayRuntimeInstance) tab.getProperties().get("application")); + //When @DockItem s are initialized, their models aren't ready yet. + //On startup, the DockItemWithInput will show up but its DisplayModel will be null. + //block until the model is ready, in an individual background thread. + try { + new Thread(() -> { + try { + instance.getRepresentation_init().get(); + if (!activeWindowsAndTabs.containsKey(window)) { + activeWindowsAndTabs.put(window, new ActiveTabsService(window)); + } + DockItemWithInput diwi = (DockItemWithInput)tab; + activeWindowsAndTabs.get(window).add(diwi); + + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + } else if (change.wasRemoved()) { + Window window = change.getList().get(0).getTabPane().getScene().getWindow(); + for(Tab tab: change.getRemoved()){ + if(activeWindowsAndTabs.get(window) != null && activeWindowsAndTabs.get(window).contains((DockItemWithInput) tab)){ + activeWindowsAndTabs.get(window).remove((DockItemWithInput) tab); + } + } + } + } + } + }; + + + ListChangeListener UXAWindowChangeListener = new ListChangeListener<>() { + @Override + public void onChanged(Change change) { + while (change.next()) { + if (change.wasAdded()) { + for (javafx.stage.Window window : change.getAddedSubList()) { + if(window.getProperties().containsKey(DockStage.KEY_ID)){ + activeWindowsAndTabs.put(window, new ActiveTabsService(window)); + for(DockPane item: DockStage.getDockPanes((Stage)window)){ + //initialize + for (Tab tab: item.getTabs()){ + if(tab instanceof DockItemWithInput){ + try { + activeWindowsAndTabs.get(window).add((DockItemWithInput)tab); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + //tack on a listener for changes + item.getTabs().addListener(UXATabChangeListener); + } + } + } + } + else if(change.wasRemoved()){ + for(Window window: change.getRemoved()){ + if(window.getProperties().containsKey(DockStage.KEY_ID)){ + activeWindowsAndTabs.remove(window); + } + } + } + } + } + }; + + private ActiveWindowsService() { + } + + //this singleton will be the exclusive communicator with the window list + public static ActiveWindowsService getInstance() { + if(instance == null){ + instance = new ActiveWindowsService(); + instance.start(); + instance.started = true; + } + return instance; + } + + public boolean isStarted() { + return started; + } + + private void start() { + instance = this; + javafx.stage.Window.getWindows().addListener(UXAWindowChangeListener); + } + + public ActiveTabsService getTabsForWindow(Window window){ + return activeWindowsAndTabs.get(window); + } + +} \ No newline at end of file diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java new file mode 100644 index 0000000000..ff40e8040e --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java @@ -0,0 +1,23 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.properties.ActionInfo; +import org.csstudio.display.builder.representation.ToolkitListener; + +public class GraphDatabaseToolkitListener implements ToolkitListener { + + @Override + public void handleAction(Widget widget, ActionInfo action) { + System.out.println("Action"); + } + + @Override + public void handleWrite(Widget widget, Object value) { + System.out.println("wrote"); + } + + @Override + public void handleClick(Widget widget, boolean with_control) { + System.out.println("Clicky"); + } +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java new file mode 100644 index 0000000000..2b6e60630b --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java @@ -0,0 +1,38 @@ +package org.phoebus.applications.uxanalytics.monitor; + + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; + +import javafx.stage.Stage; +import javafx.stage.Window; +import javafx.application.Application; +import org.csstudio.display.builder.representation.ToolkitListener; +import org.csstudio.display.builder.runtime.RuntimeUtil; +import org.csstudio.display.builder.runtime.spi.WidgetRuntimesService; + +/** + * Singleton Class to capture UI events (clicks, setting changes, Display open/close, Driver address update) + * This single monitor dispatches events to all registered observers + */ +public class UXAMonitor{ + private static UXAMonitor instance = null; + private ArrayList activeStages; + private static ActiveWindowsService activeWindowsService; + private static final ExecutorService executor = RuntimeUtil.getExecutor(); + + private UXAMonitor() { + activeWindowsService = ActiveWindowsService.getInstance(); + } + + public static UXAMonitor getInstance() { + if (instance == null) { + instance = new UXAMonitor(); + } + return instance; + } + + +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXWidgetWrapper.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXWidgetWrapper.java new file mode 100644 index 0000000000..1f38ae0559 --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXWidgetWrapper.java @@ -0,0 +1,17 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import javafx.scene.Node; +import javafx.stage.Window; +import org.csstudio.display.builder.model.Widget; +import org.phoebus.ui.docking.DockItemWithInput; + + +public abstract class UXWidgetWrapper { + + Node jfxNode; + Window window; + Widget widget; + DockItemWithInput parentTab; + + public abstract void createListener(); +} diff --git a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAController.java b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAController.java new file mode 100644 index 0000000000..74d34fd143 --- /dev/null +++ b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAController.java @@ -0,0 +1,81 @@ +package org.phoebus.applications.uxanalytics.ui; + +import javafx.event.Event; +import javafx.fxml.FXML; +import javafx.scene.control.*; + +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.GraphDatabase; + +import org.phoebus.applications.uxanalytics.monitor.UXAMonitor; + +import java.util.logging.Level; + +import static org.phoebus.applications.uxanalytics.ui.UXAnalyticsUI.logger; + +public class UXAController { + + UXAMonitor observer; + + public void setObserver(UXAMonitor observer) { + this.observer = observer; + } + + @FXML + TextField txtHost; + + @FXML + TextField txtPort; + + @FXML + TextField txtUser; + + @FXML + PasswordField passPassword; + + @FXML + Button btnConnect; + + @FXML + Label lblSuccessFailure; + + @FXML + public int tryConnect(Event event) { + lblSuccessFailure.setVisible(false); + try { + String host = txtHost.getText(); + if (host.isEmpty()) { + lblSuccessFailure.setText("Set a host name."); + lblSuccessFailure.setVisible(true); + return 1; + } + host = "neo4j://" + host; + String port = txtPort.getText(); + if (port.isEmpty() || !port.matches("\\d+")) { + lblSuccessFailure.setText("Set a valid port number."); + lblSuccessFailure.setVisible(true); + return 1; + } + host = host + ":" + port; + + String user = txtUser.getText(); + if (user.isEmpty()) { + lblSuccessFailure.setText("Set a user name."); + lblSuccessFailure.setVisible(true); + return 1; + } + String pass = passPassword.getText(); + try (var driver = GraphDatabase.driver(host, AuthTokens.basic(user, pass))) { + driver.verifyConnectivity(); + lblSuccessFailure.setText("Connected to server."); + lblSuccessFailure.setVisible(true); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to connect to server", e); + lblSuccessFailure.setText("Failed to connect to server."); + lblSuccessFailure.setVisible(true); + return 1; + } + return 0; + } +} diff --git a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java index 1b006cf6b2..d1b732f96a 100644 --- a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java +++ b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java @@ -5,6 +5,8 @@ import java.util.logging.Level; import java.util.logging.Logger; +import org.phoebus.applications.uxanalytics.monitor.GraphMonitorObserver; +import org.phoebus.applications.uxanalytics.monitor.UXAMonitor; import org.phoebus.framework.spi.AppInstance; import org.phoebus.framework.spi.AppResourceDescriptor; @@ -21,7 +23,9 @@ public class UXAnalyticsUI implements AppResourceDescriptor { public static final Logger logger = Logger.getLogger(UXAnalyticsUI.class.getPackageName()); public static final String NAME = "uxanalyticsconfig"; public static final String DISPLAY_NAME = "UX Analytics Config"; - + private final UXAController controller = new UXAController(); + private final UXAMonitor monitor = UXAMonitor.getInstance(); + private GraphMonitorObserver graphMonitorObserver = new GraphMonitorObserver(); @Override public AppInstance create(URI resource) { return create(); @@ -39,7 +43,7 @@ public String getDisplayName() { @Override public void start(){ - System.out.println("Starting my thing"); + logger.log(Level.INFO, "Load UX Analytics AppResource"); } @Override @@ -47,10 +51,15 @@ public AppInstance create() { try{ final FXMLLoader loader = new FXMLLoader(); loader.setLocation(UXAnalyticsUI.class.getResource("uxa-settings-dialog.fxml")); + //monitor.addMonitorObserver(graphMonitorObserver); + if (loader.getController() == null) { + loader.setController(controller); + controller.setObserver(monitor); + } Parent root = loader.load(); - Scene scene = new Scene(root, 600, 800); + Scene scene = new Scene(root); Stage stage = new Stage(); - stage.setTitle("Test"); + stage.setTitle("UX Analytics Configuration"); stage.setScene(scene); stage.show(); } From afc27a4774247f0f6ad2fb2599e598590573eaf2 Mon Sep 17 00:00:00 2001 From: Evan Daykin Date: Wed, 29 May 2024 11:12:34 -0400 Subject: [PATCH 03/20] sort-of fixed duplicate listener issue --- .../display/builder/runtime/RuntimeUtil.java | 1 + .../runtime/app/DisplayRuntimeInstance.java | 4 ++ .../runtime/app/DoubleEventExample.java | 18 +++++ .../monitor/ActiveTabsService.java | 41 +++++++----- .../monitor/ActiveWidgetsService.java | 19 ++++-- .../monitor/ActiveWindowsService.java | 66 ++++++++++++------- .../monitor/GraphDatabaseToolkitListener.java | 6 +- .../uxanalytics/monitor/UXAMonitor.java | 3 +- 8 files changed, 112 insertions(+), 46 deletions(-) create mode 100644 app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DoubleEventExample.java diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/RuntimeUtil.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/RuntimeUtil.java index 21382b9fe7..6a5adb2c9b 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/RuntimeUtil.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/RuntimeUtil.java @@ -15,6 +15,7 @@ import org.csstudio.display.builder.model.util.NamedDaemonPool; import org.csstudio.display.builder.representation.ToolkitListener; import org.csstudio.display.builder.representation.ToolkitRepresentation; +import org.csstudio.display.builder.runtime.app.DoubleEventExample; import org.csstudio.display.builder.runtime.script.internal.ScriptSupport; import java.util.List; diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java index e59368e215..704b46f0cf 100644 --- a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DisplayRuntimeInstance.java @@ -535,6 +535,10 @@ public void addListener(ToolkitListener listener){ this.getRepresentation().addListener(listener); } + public void removeListener(ToolkitListener listener){ + this.getRepresentation().removeListener(listener); + } + @Override public Optional getPositionAndSizeHint() { return Optional.ofNullable(active_model).flatMap(displayModel -> { diff --git a/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DoubleEventExample.java b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DoubleEventExample.java new file mode 100644 index 0000000000..8554b31078 --- /dev/null +++ b/app/display/runtime/src/main/java/org/csstudio/display/builder/runtime/app/DoubleEventExample.java @@ -0,0 +1,18 @@ +package org.csstudio.display.builder.runtime.app; + +import org.csstudio.display.builder.model.Widget; +import org.csstudio.display.builder.model.properties.ActionInfo; +import org.csstudio.display.builder.representation.ToolkitListener; + +import java.awt.*; +import java.util.logging.Level; +import java.util.logging.Logger; +import static org.csstudio.display.builder.runtime.WidgetRuntime.logger; + +public class DoubleEventExample implements ToolkitListener { + + @Override + public void handleWrite(Widget widget, Object value) { + System.out.println("Got a write event"); + } +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java index 19458d8e76..dbccd5f444 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java @@ -12,40 +12,43 @@ import org.phoebus.ui.docking.DockItemWithInput; import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.function.Supplier; public class ActiveTabsService { private final ActiveWindowsService activeWindowsService; - private final Window window; - private final ObservableMap activeTabs = FXCollections.observableHashMap(); + private final Window parentWindow; + private final ConcurrentHashMap activeTabs = new ConcurrentHashMap<>(); public ActiveTabsService(Window window){ - this.window = window; + this.parentWindow = window; activeWindowsService = ActiveWindowsService.getInstance(); } - public void add(DockItemWithInput tab) throws Exception { - if (!activeTabs.containsKey(tab)){ - activeTabs.put(tab, new ActiveWidgetsService(tab)); - addAllWidgetsIn(tab); - } + public synchronized void add(DockItemWithInput tab) throws Exception { + this.remove(tab); + activeTabs.putIfAbsent(tab.toString(), new ActiveWidgetsService(tab)); + addAllWidgetsIn(tab); } - public void remove(DockItemWithInput tab){ - activeTabs.remove(tab); + public synchronized void remove(DockItemWithInput tab){ + if(activeTabs.containsKey(tab.toString())){ + activeTabs.get(tab.toString()).close(); + activeTabs.remove(tab.toString()); + } } public boolean contains(DockItemWithInput tab){ - return activeTabs.containsKey(tab); + return activeTabs.containsKey(tab.toString()); } - public void addWidget(DockItemWithInput tab, Widget widget){ - if(activeTabs.containsKey(tab)){ - activeTabs.get(tab).add(widget); - } + public synchronized void addWidget(DockItemWithInput tab, Widget widget){ + activeTabs.get(tab.toString()).add(widget); } - public void addAllWidgetsIn(DockItemWithInput tab) throws Exception { + public synchronized void addAllWidgetsIn(DockItemWithInput tab) throws Exception { DisplayRuntimeInstance instance = (DisplayRuntimeInstance) tab.getProperties().get("application"); for(Widget widget: instance.getActiveModel().getChildren()){ if(widget instanceof EmbeddedDisplayWidget){ @@ -57,7 +60,7 @@ public void addAllWidgetsIn(DockItemWithInput tab) throws Exception { } } - public void addAllWidgetsIn(EmbeddedDisplayWidget widget, DockItemWithInput parentTab) throws Exception { + public synchronized void addAllWidgetsIn(EmbeddedDisplayWidget widget, DockItemWithInput parentTab) throws Exception { DisplayModel model = (DisplayModel) widget.getProperty("embedded_model").getValue(); for(Widget embeddedWidget: model.getChildren()){ if(embeddedWidget instanceof EmbeddedDisplayWidget){ @@ -70,4 +73,8 @@ public void addAllWidgetsIn(EmbeddedDisplayWidget widget, DockItemWithInput pare } } + + public ConcurrentHashMap getActiveTabs() { + return activeTabs; + } } diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java index 1cdacf241b..f2fb75077d 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java @@ -6,30 +6,41 @@ import org.phoebus.ui.docking.DockItemWithInput; import java.util.ArrayList; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Future; +import java.util.function.Supplier; public class ActiveWidgetsService { - private final ArrayList widgets; + private final ConcurrentLinkedDeque widgets; private final DockItemWithInput parentTab; private final ToolkitListener listener; + private final Supplier> ok_to_close = () -> { + this.close(); + return null; + }; public ActiveWidgetsService(DockItemWithInput tab){ - widgets = new ArrayList<>(); + widgets = new ConcurrentLinkedDeque<>(); parentTab = tab; listener = new GraphDatabaseToolkitListener(); ((DisplayRuntimeInstance)tab.getProperties().get("application")).addListener(listener); + parentTab.addCloseCheck(ok_to_close); } public ToolkitListener getListener(){ return listener; } - public void add(Widget widget){ + public synchronized void add(Widget widget){ widgets.add(widget); } - public void remove(Widget widget){ + public synchronized void remove(Widget widget){ widgets.remove(widget); } + public synchronized void close(){ + ((DisplayRuntimeInstance)parentTab.getProperties().get("application")).removeListener(listener); + } } diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java index 9335fed152..2e530507f3 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java @@ -12,7 +12,10 @@ import org.phoebus.ui.docking.DockPane; import org.phoebus.ui.docking.DockStage; +import javax.sound.midi.SysexMessage; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.ReentrantLock; /* @@ -25,11 +28,13 @@ public class ActiveWindowsService { private boolean started = false; private static ActiveWindowsService instance = null; - private final ObservableMap activeWindowsAndTabs = FXCollections.observableHashMap(); + private final ConcurrentHashMap activeWindowsAndTabs = new ConcurrentHashMap<>(); + private final ReentrantLock lock = new ReentrantLock(); + //don't want anyone faffing about with the map, but they should be able to see it - public UnmodifiableObservableMap getActiveWindowsAndTabs() { - return new UnmodifiableObservableMap<>(activeWindowsAndTabs); + public ConcurrentHashMap getActiveWindowsAndTabs() { + return activeWindowsAndTabs; } ListChangeListener UXATabChangeListener = new ListChangeListener<>() { @@ -39,20 +44,30 @@ public void onChanged(Change change) { if(change.wasAdded()){ for(Tab tab: change.getAddedSubList()){ Window window = change.getList().get(0).getTabPane().getScene().getWindow(); - if(tab.getProperties().get("application") instanceof DisplayRuntimeInstance){ - DisplayRuntimeInstance instance = ((DisplayRuntimeInstance) tab.getProperties().get("application")); + if(tab.getProperties().get("application") instanceof DisplayRuntimeInstance && tab instanceof DockItemWithInput){ + //When @DockItem s are initialized, their models aren't ready yet. //On startup, the DockItemWithInput will show up but its DisplayModel will be null. //block until the model is ready, in an individual background thread. try { new Thread(() -> { try { + DisplayRuntimeInstance instance = ((DisplayRuntimeInstance) tab.getProperties().get("application")); instance.getRepresentation_init().get(); - if (!activeWindowsAndTabs.containsKey(window)) { - activeWindowsAndTabs.put(window, new ActiveTabsService(window)); + lock.lock(); + String windowID = (String) window.getProperties().get(DockStage.KEY_ID); + + //FIXME: this shouldn't happen, I think + if(!activeWindowsAndTabs.containsKey(windowID)){ + System.out.println("Window "+windowID+ "("+window.getClass()+ ") not found in activeWindowsAndTabs, adding it now."); + activeWindowsAndTabs.putIfAbsent(windowID, new ActiveTabsService(window)); } + DockItemWithInput diwi = (DockItemWithInput)tab; - activeWindowsAndTabs.get(window).add(diwi); + activeWindowsAndTabs.get(windowID).add(diwi); + lock.unlock(); + System.out.println("There are now "+activeWindowsAndTabs.get(windowID).getActiveTabs().size()+" tabs in window "+windowID); + System.out.println("There are now "+activeWindowsAndTabs.size()+" windows tracked."); } catch (Exception e) { e.printStackTrace(); @@ -65,9 +80,10 @@ public void onChanged(Change change) { } } else if (change.wasRemoved()) { Window window = change.getList().get(0).getTabPane().getScene().getWindow(); + String windowID = (String) window.getProperties().get(DockStage.KEY_ID); for(Tab tab: change.getRemoved()){ - if(activeWindowsAndTabs.get(window) != null && activeWindowsAndTabs.get(window).contains((DockItemWithInput) tab)){ - activeWindowsAndTabs.get(window).remove((DockItemWithInput) tab); + if(tab instanceof DockItemWithInput && activeWindowsAndTabs.get(windowID).contains((DockItemWithInput) tab)){ + activeWindowsAndTabs.get(windowID).remove((DockItemWithInput) tab); } } } @@ -83,27 +99,33 @@ public void onChanged(Change change) { if (change.wasAdded()) { for (javafx.stage.Window window : change.getAddedSubList()) { if(window.getProperties().containsKey(DockStage.KEY_ID)){ - activeWindowsAndTabs.put(window, new ActiveTabsService(window)); - for(DockPane item: DockStage.getDockPanes((Stage)window)){ - //initialize - for (Tab tab: item.getTabs()){ - if(tab instanceof DockItemWithInput){ - try { - activeWindowsAndTabs.get(window).add((DockItemWithInput)tab); - } catch (Exception e) { - throw new RuntimeException(e); - } + String windowID = (String) window.getProperties().get(DockStage.KEY_ID); + if(!activeWindowsAndTabs.containsKey(windowID)){ + System.out.println("Window "+windowID+ "("+window.getClass()+ ") not found in activeWindowsAndTabs, adding it now."); + } + activeWindowsAndTabs.putIfAbsent(windowID, new ActiveTabsService(window)); + for(DockPane item: DockStage.getDockPanes((Stage)window)){ + //initialize + for (Tab tab: item.getTabs()){ + if(tab instanceof DockItemWithInput){ + + try { + activeWindowsAndTabs.get(windowID).add((DockItemWithInput)tab); + } catch (Exception e) { + throw new RuntimeException(e); } } - //tack on a listener for changes - item.getTabs().addListener(UXATabChangeListener); } + //tack on a listener for changes + item.getTabs().addListener(UXATabChangeListener); + } } } } else if(change.wasRemoved()){ for(Window window: change.getRemoved()){ if(window.getProperties().containsKey(DockStage.KEY_ID)){ + System.out.println("close " + window.getProperties().get(DockStage.KEY_ID)); activeWindowsAndTabs.remove(window); } } diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java index ff40e8040e..3ac7b3c95a 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java @@ -1,9 +1,12 @@ package org.phoebus.applications.uxanalytics.monitor; +import com.sun.javafx.collections.UnmodifiableObservableMap; +import javafx.stage.Window; import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.properties.ActionInfo; import org.csstudio.display.builder.representation.ToolkitListener; + public class GraphDatabaseToolkitListener implements ToolkitListener { @Override @@ -13,7 +16,8 @@ public void handleAction(Widget widget, ActionInfo action) { @Override public void handleWrite(Widget widget, Object value) { - System.out.println("wrote"); + System.out.println("wrote from "+ widget+" from thread "+Thread.currentThread().getName()); + //UnmodifiableObservableMap m = ActiveWindowsService.getInstance().getActiveWindowsAndTabs(); } @Override diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java index 2b6e60630b..8bf38320a2 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java @@ -20,11 +20,10 @@ public class UXAMonitor{ private static UXAMonitor instance = null; private ArrayList activeStages; - private static ActiveWindowsService activeWindowsService; + private static ActiveWindowsService activeWindowsService= ActiveWindowsService.getInstance(); private static final ExecutorService executor = RuntimeUtil.getExecutor(); private UXAMonitor() { - activeWindowsService = ActiveWindowsService.getInstance(); } public static UXAMonitor getInstance() { From 1969361c13a06dd92105d4d3f7e1c72adc4d03c5 Mon Sep 17 00:00:00 2001 From: Evan Daykin Date: Wed, 29 May 2024 15:20:52 -0400 Subject: [PATCH 04/20] Listening hierarchy works, but doesn't do anything interesting yet --- .../monitor/ActiveTabsService.java | 5 +-- .../monitor/ActiveWidgetsService.java | 11 ++++--- .../monitor/ActiveWindowsService.java | 31 ++----------------- .../uxanalytics/monitor/UXAJFXListener.java | 4 +++ ...tListener.java => UXAToolkitListener.java} | 6 ++-- .../uxanalytics/monitor/UXWidgetWrapper.java | 17 ---------- 6 files changed, 18 insertions(+), 56 deletions(-) create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAJFXListener.java rename app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/{GraphDatabaseToolkitListener.java => UXAToolkitListener.java} (68%) delete mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXWidgetWrapper.java diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java index dbccd5f444..e2fcb803f5 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveTabsService.java @@ -11,6 +11,7 @@ import org.csstudio.display.builder.runtime.app.DockItemRepresentation; import org.phoebus.ui.docking.DockItemWithInput; +import javax.sound.midi.SysexMessage; import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; @@ -27,13 +28,13 @@ public ActiveTabsService(Window window){ activeWindowsService = ActiveWindowsService.getInstance(); } - public synchronized void add(DockItemWithInput tab) throws Exception { + public void add(DockItemWithInput tab) throws Exception { this.remove(tab); activeTabs.putIfAbsent(tab.toString(), new ActiveWidgetsService(tab)); addAllWidgetsIn(tab); } - public synchronized void remove(DockItemWithInput tab){ + public void remove(DockItemWithInput tab){ if(activeTabs.containsKey(tab.toString())){ activeTabs.get(tab.toString()).close(); activeTabs.remove(tab.toString()); diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java index f2fb75077d..05722ae42b 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java @@ -5,7 +5,7 @@ import org.csstudio.display.builder.runtime.app.DisplayRuntimeInstance; import org.phoebus.ui.docking.DockItemWithInput; -import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.Future; import java.util.function.Supplier; @@ -17,13 +17,13 @@ public class ActiveWidgetsService { private final ToolkitListener listener; private final Supplier> ok_to_close = () -> { this.close(); - return null; + return CompletableFuture.completedFuture(true); }; public ActiveWidgetsService(DockItemWithInput tab){ widgets = new ConcurrentLinkedDeque<>(); parentTab = tab; - listener = new GraphDatabaseToolkitListener(); + listener = new UXAToolkitListener(); ((DisplayRuntimeInstance)tab.getProperties().get("application")).addListener(listener); parentTab.addCloseCheck(ok_to_close); } @@ -41,6 +41,9 @@ public synchronized void remove(Widget widget){ } public synchronized void close(){ - ((DisplayRuntimeInstance)parentTab.getProperties().get("application")).removeListener(listener); + + DisplayRuntimeInstance instance = (DisplayRuntimeInstance) parentTab.getApplication(); + if(instance != null) + instance.removeListener(listener); } } diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java index 2e530507f3..7bb0e37d3a 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java @@ -17,13 +17,6 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.ReentrantLock; - -/* - * Get active @Window instances - * For each window, add it to a map with a set of tabs as the value, - * Attach our custom ToolkitListener to each writable Widget. - * @author Evan Daykin - */ public class ActiveWindowsService { private boolean started = false; @@ -57,18 +50,9 @@ public void onChanged(Change change) { lock.lock(); String windowID = (String) window.getProperties().get(DockStage.KEY_ID); - //FIXME: this shouldn't happen, I think - if(!activeWindowsAndTabs.containsKey(windowID)){ - System.out.println("Window "+windowID+ "("+window.getClass()+ ") not found in activeWindowsAndTabs, adding it now."); - activeWindowsAndTabs.putIfAbsent(windowID, new ActiveTabsService(window)); - } - DockItemWithInput diwi = (DockItemWithInput)tab; activeWindowsAndTabs.get(windowID).add(diwi); lock.unlock(); - System.out.println("There are now "+activeWindowsAndTabs.get(windowID).getActiveTabs().size()+" tabs in window "+windowID); - System.out.println("There are now "+activeWindowsAndTabs.size()+" windows tracked."); - } catch (Exception e) { e.printStackTrace(); } @@ -78,14 +62,6 @@ public void onChanged(Change change) { } } } - } else if (change.wasRemoved()) { - Window window = change.getList().get(0).getTabPane().getScene().getWindow(); - String windowID = (String) window.getProperties().get(DockStage.KEY_ID); - for(Tab tab: change.getRemoved()){ - if(tab instanceof DockItemWithInput && activeWindowsAndTabs.get(windowID).contains((DockItemWithInput) tab)){ - activeWindowsAndTabs.get(windowID).remove((DockItemWithInput) tab); - } - } } } } @@ -100,9 +76,6 @@ public void onChanged(Change change) { for (javafx.stage.Window window : change.getAddedSubList()) { if(window.getProperties().containsKey(DockStage.KEY_ID)){ String windowID = (String) window.getProperties().get(DockStage.KEY_ID); - if(!activeWindowsAndTabs.containsKey(windowID)){ - System.out.println("Window "+windowID+ "("+window.getClass()+ ") not found in activeWindowsAndTabs, adding it now."); - } activeWindowsAndTabs.putIfAbsent(windowID, new ActiveTabsService(window)); for(DockPane item: DockStage.getDockPanes((Stage)window)){ //initialize @@ -125,8 +98,8 @@ public void onChanged(Change change) { else if(change.wasRemoved()){ for(Window window: change.getRemoved()){ if(window.getProperties().containsKey(DockStage.KEY_ID)){ - System.out.println("close " + window.getProperties().get(DockStage.KEY_ID)); - activeWindowsAndTabs.remove(window); + String windowID = (String) window.getProperties().get(DockStage.KEY_ID); + activeWindowsAndTabs.remove(windowID); } } } diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAJFXListener.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAJFXListener.java new file mode 100644 index 0000000000..519994d632 --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAJFXListener.java @@ -0,0 +1,4 @@ +package org.phoebus.applications.uxanalytics.monitor; + +public class UXAJFXListener { +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAToolkitListener.java similarity index 68% rename from app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java rename to app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAToolkitListener.java index 3ac7b3c95a..034516b612 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/GraphDatabaseToolkitListener.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAToolkitListener.java @@ -1,23 +1,21 @@ package org.phoebus.applications.uxanalytics.monitor; -import com.sun.javafx.collections.UnmodifiableObservableMap; -import javafx.stage.Window; import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.model.properties.ActionInfo; import org.csstudio.display.builder.representation.ToolkitListener; -public class GraphDatabaseToolkitListener implements ToolkitListener { +public class UXAToolkitListener implements ToolkitListener { @Override public void handleAction(Widget widget, ActionInfo action) { System.out.println("Action"); + System.out.println(action.getType()); } @Override public void handleWrite(Widget widget, Object value) { System.out.println("wrote from "+ widget+" from thread "+Thread.currentThread().getName()); - //UnmodifiableObservableMap m = ActiveWindowsService.getInstance().getActiveWindowsAndTabs(); } @Override diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXWidgetWrapper.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXWidgetWrapper.java deleted file mode 100644 index 1f38ae0559..0000000000 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXWidgetWrapper.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.phoebus.applications.uxanalytics.monitor; - -import javafx.scene.Node; -import javafx.stage.Window; -import org.csstudio.display.builder.model.Widget; -import org.phoebus.ui.docking.DockItemWithInput; - - -public abstract class UXWidgetWrapper { - - Node jfxNode; - Window window; - Widget widget; - DockItemWithInput parentTab; - - public abstract void createListener(); -} From 13b5792b4ee6091bfb932eb9a27468e6d7f28831 Mon Sep 17 00:00:00 2001 From: Evan Daykin Date: Mon, 10 Jun 2024 12:01:19 -0400 Subject: [PATCH 05/20] begin backend connection reference implementations --- app/ux-analytics/monitor/pom.xml | 112 ++++++++++++++++++ .../monitor/ActiveWidgetsService.java | 76 +++++++++++- .../monitor/ActiveWindowsService.java | 18 ++- .../monitor/BackendConnection.java | 19 +++ .../uxanalytics/monitor/ImageClient.java | 12 ++ .../monitor/MongoDBConnection.java | 80 +++++++++++++ .../uxanalytics/monitor/Neo4JConnection.java | 50 ++++++++ .../uxanalytics/monitor/S3ImageClient.java | 82 +++++++++++++ .../uxanalytics/monitor/UXAJFXListener.java | 4 - .../uxanalytics/monitor/UXAMonitor.java | 19 ++- .../uxanalytics/monitor/UXAMouseMonitor.java | 26 ++++ app/ux-analytics/ui/pom.xml | 36 ++++++ .../uxanalytics/ui/CreateUXAMenuEntry.java | 4 +- .../uxanalytics/ui/UXAController.java | 61 ++++++---- ...XAnalyticsUI.java => UXAnalyticsMain.java} | 39 ++++-- .../org.phoebus.framework.spi.AppDescriptor | 2 +- .../uxanalytics/ui/uxa-settings-dialog.fxml | 23 ++-- 17 files changed, 603 insertions(+), 60 deletions(-) create mode 100644 app/ux-analytics/monitor/pom.xml create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/BackendConnection.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ImageClient.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/MongoDBConnection.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/Neo4JConnection.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/S3ImageClient.java delete mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAJFXListener.java create mode 100644 app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMouseMonitor.java rename app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/{UXAnalyticsUI.java => UXAnalyticsMain.java} (51%) diff --git a/app/ux-analytics/monitor/pom.xml b/app/ux-analytics/monitor/pom.xml new file mode 100644 index 0000000000..cc188fd6fe --- /dev/null +++ b/app/ux-analytics/monitor/pom.xml @@ -0,0 +1,112 @@ + + + + + org.phoebus + app-ux-analytics + 4.7.2-SNAPSHOT + + 4.0.0 + app-analytics-monitor + + + 22 + 22 + UTF-8 + + + + org.openjfx + javafx-graphics + 19 + compile + + + org.openjfx + javafx-fxml + 19 + compile + + + org.phoebus + core-framework + 4.7.2-SNAPSHOT + compile + + + org.neo4j.driver + neo4j-java-driver + 5.19.0 + + + org.slf4j + slf4j-jdk14 + + + + + org.mongodb + mongodb-driver-sync + 5.1.0 + + + org.phoebus + core-ui + 4.7.2-SNAPSHOT + compile + + + org.phoebus + app-display-model + 4.7.2-SNAPSHOT + compile + + + org.phoebus + app-display-runtime + 4.7.2-SNAPSHOT + compile + + + org.apache.httpcomponents + httpclient + 4.5.14 + compile + + + + software.amazon.awssdk + bom + 2.25.66 + pom + import + + + software.amazon.awssdk + s3 + 2.25.66 + + + org.phoebus + app-logbook-elog + 4.7.2-SNAPSHOT + compile + + + + + + + src/main/resources + + + src/main/java + + **/*.fxml + + + + + \ No newline at end of file diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java index 05722ae42b..9657f74d3f 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWidgetsService.java @@ -1,10 +1,18 @@ package org.phoebus.applications.uxanalytics.monitor; +import javafx.scene.control.Tab; import org.csstudio.display.builder.model.Widget; import org.csstudio.display.builder.representation.ToolkitListener; import org.csstudio.display.builder.runtime.app.DisplayRuntimeInstance; +import org.phoebus.framework.util.ResourceParser; import org.phoebus.ui.docking.DockItemWithInput; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.Future; @@ -14,7 +22,10 @@ public class ActiveWidgetsService { private final ConcurrentLinkedDeque widgets; private final DockItemWithInput parentTab; - private final ToolkitListener listener; + private final File fileObject; + private final String SHA256; + private final String hashFilename; + private final ToolkitListener toolkitListener; private final Supplier> ok_to_close = () -> { this.close(); return CompletableFuture.completedFuture(true); @@ -23,13 +34,58 @@ public class ActiveWidgetsService { public ActiveWidgetsService(DockItemWithInput tab){ widgets = new ConcurrentLinkedDeque<>(); parentTab = tab; - listener = new UXAToolkitListener(); - ((DisplayRuntimeInstance)tab.getProperties().get("application")).addListener(listener); + fileObject = ResourceParser.getFile(parentTab.getInput()); + SHA256 = getFileSHA256(fileObject); + hashFilename = getFileName()+getFirst8CharsSHA256(); + toolkitListener = new UXAToolkitListener(); + ((DisplayRuntimeInstance)tab.getProperties().get("application")).addListener(toolkitListener); parentTab.addCloseCheck(ok_to_close); } - public ToolkitListener getListener(){ - return listener; + public File getFileObject(){ + return fileObject; + } + + public String getFileName(){ + return fileObject.getName(); + } + + //re-implement here with Java MessageDigest, so we don't need to depend on elog + private static String getFileSHA256(File fileObject){ + try(InputStream fis = new FileInputStream(fileObject)){ + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + byte[] byteBuffer = new byte[1024]; + int bytesCount = 0; + + while((bytesCount = fis.read(byteBuffer)) != -1){ + digest.update(byteBuffer, 0, bytesCount); + } + + byte[] bytes = digest.digest(); + + StringBuilder sb = new StringBuilder(); + for(int i=0; i< bytes.length ;i++){ + sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1)); + } + + return sb.toString(); + + } catch(IOException | NoSuchAlgorithmException e){; + throw new RuntimeException(e); + } + } + + public String getSHA256(){ + return SHA256; + } + + public String getFirst8CharsSHA256(){ + return SHA256.substring(0, 8); + } + + public ToolkitListener getToolkitListener(){ + return toolkitListener; } public synchronized void add(Widget widget){ @@ -44,6 +100,14 @@ public synchronized void close(){ DisplayRuntimeInstance instance = (DisplayRuntimeInstance) parentTab.getApplication(); if(instance != null) - instance.removeListener(listener); + instance.removeListener(toolkitListener); + } + + public Tab getParentTab() { + return parentTab; + } + + public String getHashFilename() { + return hashFilename; } } diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java index 7bb0e37d3a..d976c49e89 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ActiveWindowsService.java @@ -13,6 +13,7 @@ import org.phoebus.ui.docking.DockStage; import javax.sound.midi.SysexMessage; +import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.locks.ReentrantLock; @@ -25,8 +26,7 @@ public class ActiveWindowsService { private final ReentrantLock lock = new ReentrantLock(); - //don't want anyone faffing about with the map, but they should be able to see it - public ConcurrentHashMap getActiveWindowsAndTabs() { + ConcurrentHashMap getActiveWindowsAndTabs() { return activeWindowsAndTabs; } @@ -78,10 +78,8 @@ public void onChanged(Change change) { String windowID = (String) window.getProperties().get(DockStage.KEY_ID); activeWindowsAndTabs.putIfAbsent(windowID, new ActiveTabsService(window)); for(DockPane item: DockStage.getDockPanes((Stage)window)){ - //initialize for (Tab tab: item.getTabs()){ if(tab instanceof DockItemWithInput){ - try { activeWindowsAndTabs.get(windowID).add((DockItemWithInput)tab); } catch (Exception e) { @@ -89,7 +87,6 @@ public void onChanged(Change change) { } } } - //tack on a listener for changes item.getTabs().addListener(UXATabChangeListener); } } @@ -130,7 +127,16 @@ private void start() { } public ActiveTabsService getTabsForWindow(Window window){ - return activeWindowsAndTabs.get(window); + return activeWindowsAndTabs.get((String) window.getProperties().get(DockStage.KEY_ID)); + } + + public ActiveTabsService getTabsForWindow(String windowID){ + return activeWindowsAndTabs.get(windowID); + } + + public static ActiveWidgetsService getUXAWrapperFor(DockItemWithInput tab){ + Window window = tab.getTabPane().getScene().getWindow(); + return ActiveWindowsService.getInstance().getTabsForWindow(window).getActiveTabs().get(tab.toString()); } } \ No newline at end of file diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/BackendConnection.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/BackendConnection.java new file mode 100644 index 0000000000..fe9bca60c1 --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/BackendConnection.java @@ -0,0 +1,19 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import org.csstudio.display.builder.model.properties.ActionInfo; +import org.phoebus.ui.docking.DockItemWithInput; + +import java.io.IOException; + +@FunctionalInterface +public interface BackendConnection { + public Boolean connect(String hostOrRegion, Integer port, String usernameOrAccessKey, String passwordOrSecretKey); + public default String getProtocol(){return "";} + public default String getDefaultHost(){return "localhost";} + public default String getDefaultPort(){return "";} + public default String getDefaultUsername(){return "";} + public default Integer tearDown(){return -1;} + public default void handleClick(DockItemWithInput who, Integer x, Integer y){} + public default void handleWrite(DockItemWithInput who, ActionInfo info){} + public default void handleOpenDisplay(DockItemWithInput who, ActionInfo info){} +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ImageClient.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ImageClient.java new file mode 100644 index 0000000000..d7990c71ba --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/ImageClient.java @@ -0,0 +1,12 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import java.io.File; +import java.net.URI; + +public interface ImageClient { + public static ImageClient getInstance() { + return null; + } + public Integer uploadImage(URI image, File file); + public boolean imageExists(URI image); +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/MongoDBConnection.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/MongoDBConnection.java new file mode 100644 index 0000000000..6beb25287a --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/MongoDBConnection.java @@ -0,0 +1,80 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import javafx.stage.Window; +import org.phoebus.framework.util.ResourceParser; +import org.phoebus.ui.docking.DockItemWithInput; + +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MongoDBConnection implements BackendConnection{ + + Logger logger = Logger.getLogger(MongoDBConnection.class.getName()); + + public static final String PROTOCOL = "mongodb://"; + + private MongoClient mongoClient = null; + private ImageClient imageClient = null; + + @Override + public Boolean connect(String hostname, Integer port, String username, String password) { + String uri = PROTOCOL + username + ":" + password + "@" + hostname + ":" + port.toString(); + try { + mongoClient = MongoClients.create(uri); + return true; + } catch (Exception ex) { + logger.log(Level.WARNING, "Failed to connect to " + hostname, ex); + return false; + } + } + + @Override + public String getProtocol() { + return PROTOCOL; + } + + @Override + public String getDefaultPort() { + return "27017"; + } + + public Integer connect(String host, String port, String user, String password, ImageClient imageClient) { + this.imageClient = imageClient; + if(connect(host, Integer.parseInt(port), user, password)){ + return 0; + } + return -1; + } + + public void setImageClient(ImageClient imageClient) { + this.imageClient = imageClient; + } + + public boolean hasImageConnection() { + return imageClient != null; + } + + @Override + public Integer tearDown() { + mongoClient.close(); + return 0; + } + + @Override + public void handleClick(DockItemWithInput who, Integer x, Integer y) { + ActiveWidgetsService tabWrapper = ActiveWindowsService.getUXAWrapperFor(who); + + //check if screenshot exists + URI uri = who.getInput(); + String filename = tabWrapper.getHashFileName(); + boolean screenshotExists = imageClient.imageExists(who.getInput()); + //if it doesn't, create it, add document to main table with link to the screenshot as value for 'screenshotURI', + //create a new table with URI as name, and log the click action there + + //otherwise, just log the click action x, y and timestamp + + } +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/Neo4JConnection.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/Neo4JConnection.java new file mode 100644 index 0000000000..16f93662be --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/Neo4JConnection.java @@ -0,0 +1,50 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import org.neo4j.driver.AuthTokens; +import org.neo4j.driver.Driver; +import org.neo4j.driver.GraphDatabase; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Neo4JConnection implements BackendConnection{ + + Logger logger = Logger.getLogger(Neo4JConnection.class.getName()); + Driver driver; + + public static final String PROTOCOL = "neo4j://"; + + @Override + public Boolean connect(String host, Integer port, String username, String password) { + try { + driver = GraphDatabase.driver(PROTOCOL+host, AuthTokens.basic(username, password)); + driver.verifyConnectivity(); + return true; + } catch (Exception ex) { + logger.log(Level.WARNING, "Failed to connect to " + host, ex); + return false; + } + } + + @Override + public String getProtocol() { + return PROTOCOL; + } + + @Override + public String getDefaultPort() { + return "7687"; + } + + @Override + public String getDefaultUsername() { + return "neo4j"; + } + + @Override + public Integer tearDown() { + driver.close(); + return 0; + } +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/S3ImageClient.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/S3ImageClient.java new file mode 100644 index 0000000000..82a6d48cbe --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/S3ImageClient.java @@ -0,0 +1,82 @@ +package org.phoebus.applications.uxanalytics.monitor; + + +import java.io.*; +import java.util.logging.Level; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; + +import java.net.URI; +import java.util.logging.Logger; + +public class S3ImageClient implements ImageClient{ + + S3Client s3; + static final String BUCKET_NAME = "phoebus-screenshots"; + static S3ImageClient instance; + Logger logger = Logger.getLogger(S3ImageClient.class.getName()); + + private S3ImageClient(){ + String accessKey = System.getenv("AWS_ACCESS_KEY_ID"); + String secretKey = System.getenv("AWS_SECRET_ACCESS_KEY"); + s3 = S3Client.builder() + .region(Region.US_EAST_2) + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .build(); + boolean bucketFound = false; + for (var bucket : s3.listBuckets().buckets()) { + if (bucket.name().equals(BUCKET_NAME)) { + bucketFound = true; + break; + } + } + if (!bucketFound) { + logger.log(Level.WARNING, "Bucket " + BUCKET_NAME + " not found, creating it"); + s3.createBucket(builder -> builder.bucket(BUCKET_NAME).build()); + return; + } + else{ + logger.log(Level.INFO, "Bucket " + BUCKET_NAME + " found"); + } + } + + @Override + public boolean imageExists(URI key) { + try { + HeadObjectRequest headObjectRequest = HeadObjectRequest.builder() + .bucket(BUCKET_NAME) + .key(key.toString()) + .build(); + s3.headObject(headObjectRequest); + return true; + } catch (NoSuchKeyException e) { + return false; + } + } + + @Override + public Integer uploadImage(URI imagePath, File file) { + if(!imageExists(imagePath)) { + PutObjectResponse response = s3.putObject(builder -> builder.bucket(BUCKET_NAME) + .key(imagePath.toString()) + .build(), + file.toPath()); + return response.sdkHttpResponse().statusCode(); + } + else{ + return 409; + } + } + + public static ImageClient getInstance(){ + if(instance == null){ + instance = new S3ImageClient(); + } + return instance; + } +} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAJFXListener.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAJFXListener.java deleted file mode 100644 index 519994d632..0000000000 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAJFXListener.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.phoebus.applications.uxanalytics.monitor; - -public class UXAJFXListener { -} diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java index 8bf38320a2..2be768dada 100644 --- a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMonitor.java @@ -15,17 +15,27 @@ /** * Singleton Class to capture UI events (clicks, setting changes, Display open/close, Driver address update) - * This single monitor dispatches events to all registered observers + * This maintains all the parts of the UXAMonitor. */ public class UXAMonitor{ private static UXAMonitor instance = null; private ArrayList activeStages; private static ActiveWindowsService activeWindowsService= ActiveWindowsService.getInstance(); private static final ExecutorService executor = RuntimeUtil.getExecutor(); + private BackendConnection phoebusConnection; + private BackendConnection jfxConnection; private UXAMonitor() { } + public void setPhoebusConnection(BackendConnection phoebusConnection) { + this.phoebusConnection = phoebusConnection; + } + + public void setJfxConnection(BackendConnection jfxConnection) { + this.jfxConnection = jfxConnection; + } + public static UXAMonitor getInstance() { if (instance == null) { instance = new UXAMonitor(); @@ -33,5 +43,12 @@ public static UXAMonitor getInstance() { return instance; } + public void notifyConnectionChange(BackendConnection connection){ + if(connection instanceof MongoDBConnection){ + jfxConnection = connection; + }else if(connection instanceof Neo4JConnection){ + phoebusConnection = connection; + } + } } diff --git a/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMouseMonitor.java b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMouseMonitor.java new file mode 100644 index 0000000000..29b4d99f31 --- /dev/null +++ b/app/ux-analytics/monitor/src/main/java/org/phoebus/applications/uxanalytics/monitor/UXAMouseMonitor.java @@ -0,0 +1,26 @@ +package org.phoebus.applications.uxanalytics.monitor; + +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.scene.input.MouseEvent; +import org.phoebus.ui.docking.DockItem; +import org.phoebus.ui.docking.DockItemWithInput; +import org.phoebus.ui.docking.DockPane; + +public class UXAMouseMonitor implements EventHandler{ + + private DockItemWithInput tab; + private BackendConnection backendConnection; + + public UXAMouseMonitor(DockItemWithInput tab){ + tab.getContent().addEventFilter(MouseEvent.MOUSE_CLICKED, this); + } + + @Override + public void handle(MouseEvent event) { + if(event.getEventType().equals(MouseEvent.MOUSE_CLICKED)){ + backendConnection.handleClick(tab, (int)event.getSceneX(), (int)event.getSceneY()); + event.consume(); + } + } +} diff --git a/app/ux-analytics/ui/pom.xml b/app/ux-analytics/ui/pom.xml index 223c462867..c8dacfe0a3 100644 --- a/app/ux-analytics/ui/pom.xml +++ b/app/ux-analytics/ui/pom.xml @@ -35,13 +35,49 @@ 4.7.2-SNAPSHOT compile + + org.neo4j.driver + neo4j-java-driver + 5.19.0 + + + org.slf4j + slf4j-jdk14 + + + + + org.mongodb + mongodb-driver-sync + 5.1.0 + + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + org.phoebus core-ui 4.7.2-SNAPSHOT compile + + org.phoebus + app-analytics-monitor + 4.7.2-SNAPSHOT + compile + + + org.phoebus + core-logbook + 4.7.2-SNAPSHOT + + diff --git a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/CreateUXAMenuEntry.java b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/CreateUXAMenuEntry.java index 6d2c7802a3..6e0ec24708 100644 --- a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/CreateUXAMenuEntry.java +++ b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/CreateUXAMenuEntry.java @@ -7,11 +7,11 @@ public class CreateUXAMenuEntry implements MenuEntry { @Override - public String getName() { return UXAnalyticsUI.DISPLAY_NAME;} + public String getName() { return UXAnalyticsMain.DISPLAY_NAME;} @Override public Void call() throws Exception { - ApplicationService.createInstance(UXAnalyticsUI.NAME); + ApplicationService.createInstance(UXAnalyticsMain.NAME); return null; } diff --git a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAController.java b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAController.java index 74d34fd143..ad40478f61 100644 --- a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAController.java +++ b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAController.java @@ -4,14 +4,12 @@ import javafx.fxml.FXML; import javafx.scene.control.*; -import org.neo4j.driver.AuthTokens; -import org.neo4j.driver.GraphDatabase; - +import org.phoebus.applications.uxanalytics.monitor.BackendConnection; import org.phoebus.applications.uxanalytics.monitor.UXAMonitor; import java.util.logging.Level; -import static org.phoebus.applications.uxanalytics.ui.UXAnalyticsUI.logger; +import static org.phoebus.applications.uxanalytics.ui.UXAnalyticsMain.logger; public class UXAController { @@ -21,35 +19,50 @@ public void setObserver(UXAMonitor observer) { this.observer = observer; } + BackendConnection connectionLogic; + @FXML TextField txtHost; - @FXML TextField txtPort; - @FXML TextField txtUser; - @FXML PasswordField passPassword; - @FXML Button btnConnect; - @FXML Label lblSuccessFailure; + @FXML + Label lblProtocol; + + String host; + String protocol; + + + public UXAController(BackendConnection connectionLogic) { + this.protocol = connectionLogic.getProtocol(); + this.connectionLogic = connectionLogic; + } + + @FXML + public void initialize() { + lblProtocol.setText(protocol); + txtHost.setText(connectionLogic.getDefaultHost()); + txtPort.setText(connectionLogic.getDefaultPort()); + } @FXML public int tryConnect(Event event) { lblSuccessFailure.setVisible(false); try { - String host = txtHost.getText(); + host = txtHost.getText(); if (host.isEmpty()) { lblSuccessFailure.setText("Set a host name."); lblSuccessFailure.setVisible(true); return 1; } - host = "neo4j://" + host; + host = protocol + host; String port = txtPort.getText(); if (port.isEmpty() || !port.matches("\\d+")) { lblSuccessFailure.setText("Set a valid port number."); @@ -65,17 +78,25 @@ public int tryConnect(Event event) { return 1; } String pass = passPassword.getText(); - try (var driver = GraphDatabase.driver(host, AuthTokens.basic(user, pass))) { - driver.verifyConnectivity(); - lblSuccessFailure.setText("Connected to server."); + try { + if (!connectionLogic.connect(host, Integer.parseInt(port), user, pass)) { + lblSuccessFailure.setText("Failed to connect to server."); + lblSuccessFailure.setVisible(true); + return 1; + } else { + lblSuccessFailure.setText("Connected to server."); + lblSuccessFailure.setVisible(true); + observer.notifyConnectionChange(connectionLogic); + } + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to connect to server", e); + lblSuccessFailure.setText("Failed to connect to server."); lblSuccessFailure.setVisible(true); + return 1; } - } catch (Exception e) { - logger.log(Level.WARNING, "Failed to connect to server", e); - lblSuccessFailure.setText("Failed to connect to server."); - lblSuccessFailure.setVisible(true); - return 1; + return 0; + } finally { + } - return 0; } } diff --git a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsMain.java similarity index 51% rename from app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java rename to app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsMain.java index d1b732f96a..14912df572 100644 --- a/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsUI.java +++ b/app/ux-analytics/ui/src/main/java/org/phoebus/applications/uxanalytics/ui/UXAnalyticsMain.java @@ -5,7 +5,10 @@ import java.util.logging.Level; import java.util.logging.Logger; -import org.phoebus.applications.uxanalytics.monitor.GraphMonitorObserver; +import javafx.scene.layout.VBox; +import org.phoebus.applications.uxanalytics.monitor.BackendConnection; +import org.phoebus.applications.uxanalytics.monitor.MongoDBConnection; +import org.phoebus.applications.uxanalytics.monitor.Neo4JConnection; import org.phoebus.applications.uxanalytics.monitor.UXAMonitor; import org.phoebus.framework.spi.AppInstance; import org.phoebus.framework.spi.AppResourceDescriptor; @@ -19,13 +22,15 @@ * @author Evan Daykin */ -public class UXAnalyticsUI implements AppResourceDescriptor { - public static final Logger logger = Logger.getLogger(UXAnalyticsUI.class.getPackageName()); +public class UXAnalyticsMain implements AppResourceDescriptor { + public static final Logger logger = Logger.getLogger(UXAnalyticsMain.class.getPackageName()); public static final String NAME = "uxanalyticsconfig"; public static final String DISPLAY_NAME = "UX Analytics Config"; - private final UXAController controller = new UXAController(); + private BackendConnection phoebusConnection = new Neo4JConnection(); + private BackendConnection jfxConnection = new MongoDBConnection(); + private final UXAController neo4jController = new UXAController(phoebusConnection); + private final UXAController mongoController = new UXAController(jfxConnection); private final UXAMonitor monitor = UXAMonitor.getInstance(); - private GraphMonitorObserver graphMonitorObserver = new GraphMonitorObserver(); @Override public AppInstance create(URI resource) { return create(); @@ -49,18 +54,26 @@ public void start(){ @Override public AppInstance create() { try{ - final FXMLLoader loader = new FXMLLoader(); - loader.setLocation(UXAnalyticsUI.class.getResource("uxa-settings-dialog.fxml")); - //monitor.addMonitorObserver(graphMonitorObserver); - if (loader.getController() == null) { - loader.setController(controller); - controller.setObserver(monitor); - } - Parent root = loader.load(); + final FXMLLoader log4JLoader = new FXMLLoader(); + log4JLoader.setLocation(UXAnalyticsMain.class.getResource("uxa-settings-dialog.fxml")); + log4JLoader.setController(neo4jController); + Parent log4JRoot = log4JLoader.load(); + neo4jController.setObserver(monitor); + + + final FXMLLoader mongoLoader = new FXMLLoader(); + mongoLoader.setLocation(UXAnalyticsMain.class.getResource("uxa-settings-dialog.fxml")); + mongoLoader.setController(mongoController); + Parent mongoRoot = mongoLoader.load(); + mongoController.setObserver(monitor); + + VBox root = new VBox(log4JRoot, mongoRoot); Scene scene = new Scene(root); Stage stage = new Stage(); stage.setTitle("UX Analytics Configuration"); stage.setScene(scene); + neo4jController.initialize(); + mongoController.initialize(); stage.show(); } catch (IOException e) { diff --git a/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor b/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor index aec13ca465..b2c22aa3e0 100644 --- a/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor +++ b/app/ux-analytics/ui/src/main/resources/META-INF/services/org.phoebus.framework.spi.AppDescriptor @@ -1 +1 @@ -org.phoebus.applications.uxanalytics.ui.UXAnalyticsUI +org.phoebus.applications.uxanalytics.ui.UXAnalyticsMain diff --git a/app/ux-analytics/ui/src/main/resources/org/phoebus/applications/uxanalytics/ui/uxa-settings-dialog.fxml b/app/ux-analytics/ui/src/main/resources/org/phoebus/applications/uxanalytics/ui/uxa-settings-dialog.fxml index fe27b2eebe..a9abf3fbc3 100644 --- a/app/ux-analytics/ui/src/main/resources/org/phoebus/applications/uxanalytics/ui/uxa-settings-dialog.fxml +++ b/app/ux-analytics/ui/src/main/resources/org/phoebus/applications/uxanalytics/ui/uxa-settings-dialog.fxml @@ -1,14 +1,23 @@ + + + + - - - - + - - - + + + + + + +