diff --git a/app/save-and-restore/app/doc/images/compare_to_archiver.png b/app/save-and-restore/app/doc/images/compare_to_archiver.png new file mode 100644 index 0000000000..651a16fb83 Binary files /dev/null and b/app/save-and-restore/app/doc/images/compare_to_archiver.png differ diff --git a/app/save-and-restore/app/doc/images/date_time_picker.png b/app/save-and-restore/app/doc/images/date_time_picker.png new file mode 100644 index 0000000000..9a79d94bd4 Binary files /dev/null and b/app/save-and-restore/app/doc/images/date_time_picker.png differ diff --git a/app/save-and-restore/app/doc/index.rst b/app/save-and-restore/app/doc/index.rst index cb38cbdcea..f0eefffaa9 100644 --- a/app/save-and-restore/app/doc/index.rst +++ b/app/save-and-restore/app/doc/index.rst @@ -308,6 +308,22 @@ the :math:`{\Delta}` Base Snapshot column will show the difference to the refere .. image:: images/compare-snapshots-view.png :width: 80% +Compare to archiver data +------------------------ + +In the context menu of a tab showing a snapshot user can chose to compare the snapshot to data retrieved from an +archiver, if one is configured: + +.. image:: images/compare_to_archiver.png + +Selecting this item will trigger a date/time picker where user can specify the point in time for which to get +archiver data: + +.. image:: images/date_time_picker.png + +Once data has been returned from the archiver service, it will be rendered as a snapshot in the comparison view. + +**NOTE:** If the archiver does not contain a PV, it will be rendered as DISCONNECTED in the view. Search And Filters ------------------ diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java index 38655f4afb..5a1dd2a159 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/Messages.java @@ -26,6 +26,7 @@ public class Messages { public static String actionOpenNodeDescription; public static String alertContinue; public static String alertAddingPVsToConfiguration; + public static String archiver; public static String authenticationFailed; public static String baseSetpoint; public static String buttonSearch; @@ -38,6 +39,7 @@ public class Messages { public static String contextMenuAddTagWithComment; public static String contextMenuCreateSnapshot; public static String contextMenuCompareSnapshots; + public static String contextMenuCompareSnapshotWithArchiverData; public static String contextMenuDelete; public static String copy; @@ -65,6 +67,7 @@ public class Messages { public static String currentPVValue; public static String currentReadbackValue; public static String currentSetpointValue; + public static String dateTimePickerTitle; public static String deleteFilter; public static String deleteFilterFailed; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java index 631bfb52d8..a392ac2cf6 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreController.java @@ -36,6 +36,7 @@ import javafx.scene.control.ButtonType; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; +import javafx.scene.control.Dialog; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.Menu; @@ -85,6 +86,7 @@ import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; import org.phoebus.ui.javafx.ImageCache; +import org.phoebus.ui.time.DateTimePane; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; @@ -95,6 +97,7 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -385,6 +388,7 @@ protected void compareSnapshot() { compareSnapshot(browserSelectionModel.getSelectedItems().get(0).getValue()); } + /** * Action when user requests comparison between an opened snapshot and the specifies snapshot {@link Node} * diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java index 2a0e979a2b..528153e75d 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreService.java @@ -18,6 +18,7 @@ package org.phoebus.applications.saveandrestore.ui; +import org.epics.vtype.VType; import org.phoebus.applications.saveandrestore.model.*; import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; import org.phoebus.applications.saveandrestore.model.Configuration; @@ -35,8 +36,12 @@ import org.phoebus.applications.saveandrestore.client.SaveAndRestoreJerseyClient; import org.phoebus.core.vtypes.VDisconnectedData; +import org.phoebus.pv.PV; +import org.phoebus.pv.PVPool; +import org.phoebus.util.time.TimestampFormats; import javax.ws.rs.core.MultivaluedMap; +import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -419,6 +424,7 @@ public List restore(String snapshotNodeId) throws Exception{ /** * Requests service to take a snapshot, i.e. to read PVs as defined in a {@link Configuration}. + * This should be called off the UI thread. * @param configurationNodeId The unique id of the {@link Configuration} for which to take the snapshot * @return A {@link List} of {@link SnapshotItem}s carrying snapshot values read by the service. */ @@ -427,4 +433,58 @@ public List takeSnapshot(String configurationNodeId) throws Except executor.submit(() -> saveAndRestoreClient.takeSnapshot(configurationNodeId)); return future.get(); } + + /** + * Requests service to take a snapshot, i.e. to read PVs as defined in a {@link Configuration}. + * This should be called off the UI thread. + * @param configurationNodeId The unique id of the {@link Configuration} for which to take the snapshot + * @param time If non-null, the snapshot is created from archived values. + * @return A {@link List} of {@link SnapshotItem}s carrying snapshot values read by the service or read + * from an archiver. + */ + public List takeSnapshot(String configurationNodeId, Instant time) throws Exception{ + if(time == null){ + return takeSnapshot(configurationNodeId); + } + else{ + ConfigurationData configNode = getConfiguration(configurationNodeId); + List configPvList = configNode.getPvList(); + List snapshotItems = new ArrayList<>(); + configPvList.forEach(configPv -> { + SnapshotItem snapshotItem = new SnapshotItem(); + snapshotItem.setConfigPv(configPv); + snapshotItem.setValue(readFromArchiver(configPv.getPvName(), time)); + if(configPv.getReadbackPvName() != null){ + snapshotItem.setValue(readFromArchiver(configPv.getReadbackPvName(), time)); + } + snapshotItems.add(snapshotItem); + }); + return snapshotItems; + } + } + + /** + * Reads the PV value from archiver. + * @param pvName Name of PV, scheme like for instance pva:// will be removed. + * @param time The point in time supplied in the archiver request + * @return A {@link VType} value if archiver contains the wanted data, otherwise {@link VDisconnectedData}. + */ + private VType readFromArchiver(String pvName, Instant time){ + // Check if pv name is prefixed with a scheme, e.g. pva://, ca://... + int indexSchemeSeparator = pvName.indexOf("://"); + if(indexSchemeSeparator > 0 && pvName.length() > indexSchemeSeparator){ + pvName = pvName.substring(indexSchemeSeparator + 1); + } + // Prepend "alarm://" + pvName = "archive://" + pvName + "(" + TimestampFormats.SECONDS_FORMAT.format(time) + ")"; + try { + PV pv = PVPool.getPV(pvName); + VType pvValue = pv.read(); + PVPool.releasePV(pv); + return pvValue; + } catch (Exception e) { + // Not found in archiver + return VDisconnectedData.INSTANCE; + } + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VDisconnectedData.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VDisconnectedData.java deleted file mode 100644 index 3a3a997918..0000000000 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/VDisconnectedData.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2020 European Spallation Source ERIC. - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package org.phoebus.applications.saveandrestore.ui; - -import org.epics.vtype.Alarm; -import org.epics.vtype.AlarmProvider; -import org.epics.vtype.VType; - -/** - * - * VDisconnectedData represents a {@link VType} for a disconnected PV, where the data type is not known. - * - * @author Jaka Bobnar - * - */ -public final class VDisconnectedData extends VType implements AlarmProvider { - - private static final long serialVersionUID = -2399970529728581034L; - - /** The singleton instance */ - public static final VDisconnectedData INSTANCE = new VDisconnectedData(); - - private static final String TO_STRING = "---"; - public static final String DISCONNECTED = "DISCONNECTED"; - - private VDisconnectedData() { - } - - /* - * (non-Javadoc) - * - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return TO_STRING; - } - - @Override - public Alarm getAlarm() { - return Alarm.disconnected(); - } - - - public String getName(){ return "";} - -} diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java index ce9d79c4b4..4e5092a4d9 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotController.java @@ -24,6 +24,7 @@ import javafx.fxml.FXML; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; import javafx.scene.layout.BorderPane; import javafx.scene.layout.VBox; import org.epics.vtype.*; @@ -37,7 +38,9 @@ import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.ui.dialog.DialogHelper; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.phoebus.ui.time.DateTimePane; +import java.time.Instant; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; @@ -378,16 +381,66 @@ private void showFailedRestoreResult(List restoreResultList){ }); } + /** + * Adds a snapshot for the sake of comparison with the one currently in view. + * @param snapshotNode A snapshot {@link Node} selected by user in the {@link javafx.scene.control.TreeView}, + * i.e. a snapshot previously persisten in the service. + */ public void addSnapshot(Node snapshotNode) { disabledUi.set(true); - try { - Snapshot snapshot = getSnapshotFromService(snapshotNode); - snapshotTableViewController.addSnapshot(snapshot); - } catch (Exception e) { - Logger.getLogger(SnapshotController.class.getName()).log(Level.WARNING, "Failed to add snapshot", e); - } finally { - disabledUi.set(false); + JobManager.schedule("Add snapshot", monitor -> { + try { + Snapshot snapshot = getSnapshotFromService(snapshotNode); + Platform.runLater(() -> snapshotTableViewController.addSnapshot(snapshot)); + } catch (Exception e) { + Logger.getLogger(SnapshotController.class.getName()).log(Level.WARNING, "Failed to add snapshot", e); + } finally { + disabledUi.set(false); + } + }); + } + + /** + * Launches a date/time picker and then reads from archiver to construct an in-memory {@link Snapshot} used for comparison. + * @param configurationNode A {@link Node} of type {@link NodeType#CONFIGURATION}. + */ + public void addSnapshotFromArchiver(Node configurationNode){ + DateTimePane dateTimePane = new DateTimePane(); + Dialog timePickerDialog = new Dialog<>(); + timePickerDialog.setTitle(Messages.dateTimePickerTitle); + timePickerDialog.getDialogPane().setContent(dateTimePane); + timePickerDialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + timePickerDialog.setResultConverter(b -> { + if(b.equals(ButtonType.OK)){ + return dateTimePane.getInstant(); + } + return null; + }); + Instant time = timePickerDialog.showAndWait().get(); + if(time == null){ // User cancels date/time picker dialog + return; } + disabledUi.set(true); + JobManager.schedule("Add snapshot from archiver", monitor -> { + List snapshotItems; + try { + snapshotItems = SaveAndRestoreService.getInstance().takeSnapshot(configurationNode.getUniqueId(), time); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to query archiver for data", e); + disabledUi.set(false); + return; + } + Snapshot snapshot = new Snapshot(); + snapshot.setSnapshotNode(Node.builder().nodeType(NodeType.SNAPSHOT).name(Messages.archiver).created(new Date(time.toEpochMilli())).build()); + SnapshotData snapshotData = new SnapshotData(); + snapshotData.setUniqueId("anonymous"); + snapshotData.setSnapshotItems(snapshotItems); + snapshot.setSnapshotData(snapshotData); + Platform.runLater(() -> { + snapshotTableViewController.addSnapshot(snapshot); + disabledUi.set(false); + }); + }); } private Snapshot getSnapshotFromService(Node snapshotNode) throws Exception { diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java index 705d60aa32..284ab469cc 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotControlsViewController.java @@ -40,6 +40,8 @@ import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.event.SaveAndRestoreEventReceiver; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreBaseController; +import org.phoebus.pv.PV; +import org.phoebus.pv.PVPool; import org.phoebus.ui.docking.DockPane; import org.phoebus.util.time.TimestampFormats; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTab.java index 07abcc0ffb..f5ef3721a8 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTab.java @@ -18,22 +18,29 @@ package org.phoebus.applications.saveandrestore.ui.snapshot; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleObjectProperty; import javafx.fxml.FXMLLoader; +import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import org.phoebus.applications.saveandrestore.Messages; import org.phoebus.applications.saveandrestore.model.Node; import org.phoebus.applications.saveandrestore.model.NodeType; +import org.phoebus.applications.saveandrestore.model.Snapshot; import org.phoebus.applications.saveandrestore.model.Tag; import org.phoebus.applications.saveandrestore.ui.ImageRepository; +import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreController; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreService; import org.phoebus.applications.saveandrestore.ui.SaveAndRestoreTab; +import org.phoebus.applications.saveandrestore.ui.VNoData; import org.phoebus.framework.nls.NLS; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; +import org.phoebus.ui.javafx.ImageCache; import java.io.IOException; +import java.time.Instant; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; @@ -53,6 +60,7 @@ public class SnapshotTab extends SaveAndRestoreTab { private final SimpleObjectProperty tabGraphicImageProperty = new SimpleObjectProperty<>(); + protected Image compareSnapshotIcon = ImageCache.getImage(SaveAndRestoreController.class, "/icons/save-and-restore/compare.png"); public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, SaveAndRestoreService saveAndRestoreService) { @@ -109,6 +117,25 @@ public SnapshotTab(org.phoebus.applications.saveandrestore.model.Node node, Save } }); + MenuItem compareSnapshotToArchiverDataMenuItem = new MenuItem(Messages.contextMenuCompareSnapshotWithArchiverData, new ImageView(compareSnapshotIcon)); + compareSnapshotToArchiverDataMenuItem.setOnAction(ae -> + addSnapshotFromArchive(((SnapshotController)controller).getConfigurationNode())); + + // If the view has been launched to take a new snapshot, there is no snapshot data to compare to, + // consequently the menu item is disabled. Also disabled if the configuration does not have any PVs. + getContextMenu().setOnShowing(e -> { + Snapshot snapshot = ((SnapshotController)controller).getSnapshot(); + if(snapshot.getSnapshotData().getSnapshotItems().isEmpty()){ + compareSnapshotToArchiverDataMenuItem.disableProperty().set(true); + } + else if(snapshot.getSnapshotData().getSnapshotItems().get(0).getValue().equals(VNoData.INSTANCE)){ + compareSnapshotToArchiverDataMenuItem.disableProperty().set(true); + } + else{ + compareSnapshotToArchiverDataMenuItem.disableProperty().set(false); + } + }); + getContextMenu().getItems().add(compareSnapshotToArchiverDataMenuItem); SaveAndRestoreService.getInstance().addNodeChangeListener(this); } @@ -158,10 +185,22 @@ public void loadSnapshot(Node snapshotNode) { ((SnapshotController) controller).loadSnapshot(snapshotNode); } - public void addSnapshot(org.phoebus.applications.saveandrestore.model.Node node) { + /** + * Adds a user selected snapshot for the sake of comparison + * @param node A {@link Node} of type {@link NodeType#SNAPSHOT} + */ + public void addSnapshot(Node node) { ((SnapshotController) controller).addSnapshot(node); } + /** + * Adds a {@link Snapshot} created from archiver data, for the sake of comparison + * @param configurationNode A {@link Node} of type {@link NodeType#CONFIGURATION}. + */ + private void addSnapshotFromArchive(Node configurationNode){ + ((SnapshotController) controller).addSnapshotFromArchiver(configurationNode); + } + @Override public void nodeChanged(Node node) { if (node.getUniqueId().equals(getId())) { diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableViewController.java index f183746f66..41f0354ca9 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/snapshot/SnapshotTableViewController.java @@ -57,7 +57,11 @@ import org.phoebus.util.time.TimestampFormats; import java.text.SimpleDateFormat; +import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAmount; +import java.time.temporal.TemporalUnit; import java.util.ArrayList; import java.util.Collections; import java.util.Date; diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties index 57306a7171..ebf31f8549 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/messages.properties @@ -6,6 +6,7 @@ actionOpenNodeId=Node id: add=Add alertContinue=Do you wish to continue? alertAddingPVsToConfiguration=Adding PV to configuration +archiver=Archiver authenticationFailed=Authentication Failed baseSetpoint=Base Setpoint browseForLocation=Browse to select location @@ -31,6 +32,7 @@ contextMenuAddToCompositeSnapshot=Add to composite snapshot contextMenuAddTagWithComment=Add a tag with comment contextMenuCreateSnapshot=Create Snapshot contextMenuCompareSnapshots=Compare Snapshots +contextMenuCompareSnapshotWithArchiverData=Compare to Archiver data contextMenuDelete=Delete Edit=Edit contextMenuNewFolder=New Folder @@ -56,6 +58,7 @@ currentPVValue=Current PV Value currentReadbackValue=Current Readback PV Value currentSetpointValue=Current Setpoint Value cut=Cut +dateTimePickerTitle=Select date and time deleteFilter=Delete Filter deleteFilterFailed=Failed to delete filter description=Description diff --git a/core/ui/src/main/java/org/phoebus/ui/time/DateTimePane.java b/core/ui/src/main/java/org/phoebus/ui/time/DateTimePane.java index ac2b00a9d7..8cc9287c88 100644 --- a/core/ui/src/main/java/org/phoebus/ui/time/DateTimePane.java +++ b/core/ui/src/main/java/org/phoebus/ui/time/DateTimePane.java @@ -33,7 +33,7 @@ * @author Kay Kasemir */ @SuppressWarnings("nls") -class DateTimePane extends GridPane +public class DateTimePane extends GridPane { // Use TimestampFormats private static final StringConverter DATE_CONVERTER = new StringConverter<>() diff --git a/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java b/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java index 276a759f6f..0220d2a6e8 100644 --- a/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java +++ b/core/ui/src/main/java/org/phoebus/ui/time/TimeRelativeIntervalPane.java @@ -85,7 +85,7 @@ public TimeRelativeIntervalPane(final TemporalAmountPane.Type type) add(abs_end, 2, 1); rel_end.setPadding(new Insets(5)); - add(rel_end, 2, 2); + //add(rel_end, 2, 2); add(new Separator(Orientation.HORIZONTAL), 0, 3, 3, 1);