diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java index cc81f3c8d8..688a6b9b09 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClient.java @@ -19,7 +19,18 @@ package org.phoebus.applications.saveandrestore.client; import org.phoebus.applications.saveandrestore.SaveAndRestoreClientException; -import org.phoebus.applications.saveandrestore.model.*; +import org.phoebus.applications.saveandrestore.model.CompositeSnapshot; +import org.phoebus.applications.saveandrestore.model.Configuration; +import org.phoebus.applications.saveandrestore.model.ConfigurationData; +import org.phoebus.applications.saveandrestore.model.Node; +import org.phoebus.applications.saveandrestore.model.NodeType; +import org.phoebus.applications.saveandrestore.model.RestoreResult; +import org.phoebus.applications.saveandrestore.model.Snapshot; +import org.phoebus.applications.saveandrestore.model.SnapshotData; +import org.phoebus.applications.saveandrestore.model.SnapshotItem; +import org.phoebus.applications.saveandrestore.model.Tag; +import org.phoebus.applications.saveandrestore.model.TagData; +import org.phoebus.applications.saveandrestore.model.UserData; import org.phoebus.applications.saveandrestore.model.search.Filter; import org.phoebus.applications.saveandrestore.model.search.SearchResult; @@ -77,13 +88,6 @@ public interface SaveAndRestoreClient { */ Node createNewNode(String parentsUniqueId, Node node); - /** - * Updates a node, e.g. if user wishes to add or remove tags from a snapshot {@link Node} - * - * @param nodeToUpdate The {@link Node} subject to update. - * @return The updated {@link Node}. - */ - Node updateNode(Node nodeToUpdate); /** * Updates a node, e.g. if user wishes to add or remove tags from a snapshot {@link Node} @@ -128,6 +132,7 @@ public interface SaveAndRestoreClient { /** * Constructs a path like string to facilitate location of a {@link Node} in the tree structure. + * * @param uniqueNodeId Unique id * @return Path like /Root folder/foo/bar/my/favourite/node */ @@ -138,8 +143,9 @@ public interface SaveAndRestoreClient { /** * Creates a new {@link Node} of type {@link NodeType#CONFIGURATION} in the remote * service. - * @param parentNodeId Non-null and non-empty unique id of an existing parent {@link Node}, - * which must be of type {@link NodeType#FOLDER}. + * + * @param parentNodeId Non-null and non-empty unique id of an existing parent {@link Node}, + * which must be of type {@link NodeType#FOLDER}. * @param configuration {@link ConfigurationData} object * @return A representation of the persisted {@link Configuration}. */ @@ -152,8 +158,9 @@ public interface SaveAndRestoreClient { /** * Creates a {@link Snapshot} + * * @param parentNodeId The unique id of the configuration {@link Node} associated with the {@link Snapshot} - * @param snapshot The {@link Snapshot} data object. + * @param snapshot The {@link Snapshot} data object. * @return The new {@link Snapshot} as persisted by the service */ Snapshot createSnapshot(String parentNodeId, Snapshot snapshot); @@ -162,7 +169,8 @@ public interface SaveAndRestoreClient { /** * Creates a new {@link CompositeSnapshot}. - * @param parentNodeId The parent {@link Node} for the new {@link CompositeSnapshot} + * + * @param parentNodeId The parent {@link Node} for the new {@link CompositeSnapshot} * @param compositeSnapshot The data object * @return A {@link CompositeSnapshot} as persisted by the service. */ @@ -172,8 +180,9 @@ public interface SaveAndRestoreClient { * Utility for the purpose of checking whether a set of snapshots contain duplicate PV names. * The input snapshot ids may refer to {@link Node}s of types {@link org.phoebus.applications.saveandrestore.model.NodeType#SNAPSHOT} * and {@link org.phoebus.applications.saveandrestore.model.NodeType#COMPOSITE_SNAPSHOT} + * * @param snapshotNodeIds List of {@link Node} ids corresponding to {@link Node}s of types {@link org.phoebus.applications.saveandrestore.model.NodeType#SNAPSHOT} - * and {@link org.phoebus.applications.saveandrestore.model.NodeType#COMPOSITE_SNAPSHOT} + * and {@link org.phoebus.applications.saveandrestore.model.NodeType#COMPOSITE_SNAPSHOT} * @return A list of PV names that occur more than once across the list of {@link Node}s corresponding * to the input. Empty if no duplicates are found. */ @@ -182,6 +191,7 @@ public interface SaveAndRestoreClient { /** * Updates a composite snapshot. Note that the list of referenced snapshots must be the full list of wanted * snapshots, i.e. there is no way to only add new references, or only remove unwanted references. + * * @param compositeSnapshot A {@link CompositeSnapshot} object hold data. * @return The updates {@link CompositeSnapshot} object. */ @@ -189,6 +199,7 @@ public interface SaveAndRestoreClient { /** * Search for {@link Node}s based on the specified search parameters. + * * @param searchParams {@link MultivaluedMap} holding search parameters. * @return A {@link SearchResult} with potentially empty list of matching {@link Node}s */ @@ -196,6 +207,7 @@ public interface SaveAndRestoreClient { /** * Save a new or updated {@link Filter} + * * @param filter The {@link Filter} to save * @return The saved {@link Filter} */ @@ -208,12 +220,14 @@ public interface SaveAndRestoreClient { /** * Deletes a {@link Filter} based on its name. - * @param name + * + * @param name Unique name of a {@link Filter} */ void deleteFilter(String name); /** * Adds a tag to a list of unique node ids, see {@link TagData} + * * @param tagData see {@link TagData} * @return A list of updated {@link Node}s. This may contain fewer elements than the list of unique node ids * passed in the tagData parameter. @@ -222,6 +236,7 @@ public interface SaveAndRestoreClient { /** * Deletes a tag from a list of unique node ids, see {@link TagData} + * * @param tagData see {@link TagData} * @return A list of updated {@link Node}s. This may contain fewer elements than the list of unique node ids * passed in the tagData parameter. @@ -230,6 +245,7 @@ public interface SaveAndRestoreClient { /** * For the purpose of login and authentication in the service. + * * @param userName User's account name * @param password User's password * @return A {@link UserData} object if login is successful, otherwise implementation should throw @@ -239,6 +255,7 @@ public interface SaveAndRestoreClient { /** * Requests service to restore the specified {@link SnapshotItem}s + * * @param snapshotItems A {@link List} of {@link SnapshotItem}s * @return A @{@link List} of {@link RestoreResult}s with information on potentially failed {@link SnapshotItem}s. */ @@ -246,6 +263,7 @@ public interface SaveAndRestoreClient { /** * Requests service to restore the specified snapshot. + * * @param snapshotNodeId Unique id of a snapshot * @return A @{@link List} of {@link RestoreResult}s with information on potentially failed {@link SnapshotItem}s. */ @@ -253,6 +271,7 @@ public interface SaveAndRestoreClient { /** * Requests service to take a snapshot. + * * @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. */ diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClientImpl.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClientImpl.java index 786270532e..05c5755d8e 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClientImpl.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/client/SaveAndRestoreClientImpl.java @@ -42,6 +42,7 @@ import java.time.Duration; import java.util.Base64; import java.util.List; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -155,11 +156,6 @@ public Node createNewNode(String parentsUniqueId, Node node) { } } - @Override - public Node updateNode(Node nodeToUpdate) { - return updateNode(nodeToUpdate, false); - } - @Override public Node updateNode(Node nodeToUpdate, boolean customTimeForMigration) { try { 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 924e6d2a4d..940dfb72df 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 @@ -23,6 +23,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import javafx.application.Platform; import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; @@ -73,6 +75,7 @@ import org.phoebus.applications.saveandrestore.model.NodeType; import org.phoebus.applications.saveandrestore.model.Tag; import org.phoebus.applications.saveandrestore.model.search.Filter; +import org.phoebus.applications.saveandrestore.model.search.FilterActivator; import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil; import org.phoebus.applications.saveandrestore.model.search.SearchQueryUtil.Keys; import org.phoebus.applications.saveandrestore.model.search.SearchResult; @@ -124,6 +127,7 @@ import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; +import java.util.ServiceLoader; import java.util.Stack; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -160,7 +164,7 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController @SuppressWarnings("unused") @FXML - private CheckBox enableFilterCheckBox; + private CheckBox autoFilterCheckbox; @SuppressWarnings("unused") @FXML @@ -173,8 +177,9 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController protected static final Logger LOG = Logger.getLogger(SaveAndRestoreController.class.getName()); protected Comparator> treeNodeComparator; protected SimpleBooleanProperty disabledUi = new SimpleBooleanProperty(false); - private final SimpleBooleanProperty filterEnabledProperty = new SimpleBooleanProperty(false); private static final Logger logger = Logger.getLogger(SaveAndRestoreController.class.getName()); + private final ObjectProperty currentFilterProperty = new SimpleObjectProperty<>(null); + @SuppressWarnings("unused") @FXML @@ -198,6 +203,13 @@ public class SaveAndRestoreController extends SaveAndRestoreBaseController private final MenuItem deleteNodeMenuItem = new MenuItem(Messages.contextMenuDelete, ImageCache.getImageView(ImageCache.class, "/icons/delete.png")); private final MenuItem pasteMenuItem = new MenuItem(Messages.paste, ImageCache.getImageView(ImageCache.class, "/icons/paste.png")); + private final BooleanProperty autoFilterActive = new SimpleBooleanProperty(); + + /** + * Potentially empty list of {@link FilterActivator}s implementing auto selection of {@link Filter}s. + */ + private final ObservableList filterActivators = FXCollections.observableArrayList(); + List menuItems = Arrays.asList( new LoginMenuItem(this, selectedItemsProperty, @@ -241,18 +253,26 @@ public void initialize(URL url, ResourceBundle resourceBundle) { treeNodeComparator = Comparator.comparing(TreeItem::getValue); treeView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - treeView.getStylesheets().add(getClass().getResource("/save-and-restore-style.css").toExternalForm()); + treeViewPane.getStylesheets().add(getClass().getResource("/save-and-restore-style.css").toExternalForm()); browserSelectionModel = treeView.getSelectionModel(); - ImageView searchButtonImageView = ImageCache.getImageView(SaveAndRestoreApplication.class, "/icons/sar-search.png"); - searchButtonImageView.setFitWidth(20); - searchButtonImageView.setFitHeight(20); - searchButton.setGraphic(searchButtonImageView); + autoFilterCheckbox.selectedProperty().bindBidirectional(autoFilterActive); + + autoFilterActive.addListener((obs, o, n) -> { + if (n) { + // Check if a filter selection is active in any implementation. Match on first found. + Optional filterActivatorOptional = + filterActivators.stream().filter(a -> a.getActivatedFilter() != null).findFirst(); + filterActivatorOptional.ifPresent(filterActivator -> activateFilter(filterActivator.getActivatedFilter())); + } + }); + + autoFilterCheckbox.visibleProperty().bind(Bindings.createBooleanBinding(() -> + !filterActivators.isEmpty(), filterActivators)); - enableFilterCheckBox.selectedProperty().bindBidirectional(filterEnabledProperty); - filtersComboBox.disableProperty().bind(filterEnabledProperty.not()); - filterEnabledProperty.addListener((observable, oldValue, newValue) -> filterEnabledChanged(newValue)); + filtersComboBox.disableProperty().bind(Bindings.createBooleanBinding(autoFilterActive::get, autoFilterActive)); + filtersComboBox.valueProperty().bindBidirectional(currentFilterProperty); treeView.setEditable(true); @@ -279,8 +299,8 @@ public ListCell cal public void updateItem(org.phoebus.applications.saveandrestore.model.search.Filter item, boolean empty) { super.updateItem(item, empty); - if (!empty && item != null) { - setText(item.getName()); + if (!empty) { + setText(item == null ? Messages.noFilter : item.getName()); } } }; @@ -292,7 +312,7 @@ public void updateItem(org.phoebus.applications.saveandrestore.model.search.Filt @Override public String toString(Filter filter) { if (filter == null) { - return ""; + return Messages.noFilter; } else { return filter.getName(); } @@ -310,12 +330,13 @@ public Filter fromString(String s) { if (!newValue.equals(oldValue)) { applyFilter(newValue); } + } else { + clearFilter(); } }); filtersComboBox.itemsProperty().bind(new SimpleObjectProperty<>(filtersList)); - enableFilterCheckBox.disableProperty().bind(Bindings.createBooleanBinding(filtersList::isEmpty, filtersList)); // Clear clipboard to make sure that only custom data format is // considered in paste actions. @@ -347,19 +368,21 @@ public Filter fromString(String s) { webSocketClientService.setConnectCallback(this::handleWebSocketConnected); webSocketClientService.setDisconnectCallback(this::handleWebSocketDisconnected); webSocketClientService.connect(); + } /** - * Loads the data for the tree root as provided (persisted) by the current + * Pulls initial data from the service in order to configure the UI and render the {@link TreeView}. * {@link org.phoebus.applications.saveandrestore.client.SaveAndRestoreClient}. */ - public void loadTreeData() { + public void loadInitialData() { JobManager.schedule("Load save-and-restore tree data", monitor -> { Node rootNode = saveAndRestoreService.getRootNode(); treeInitializationCountDownLatch.countDown(); TreeItem rootItem = createTreeItem(rootNode); List savedTreeViewStructure = getSavedTreeStructure(); + // Check if there is a save tree structure. Also check that the first node id (=tree root) // has the same unique id as the actual root node retrieved from the remote service. This check // is needed to handle the case when the client connects to a different save-and-restore service. @@ -379,15 +402,24 @@ public void loadTreeData() { List> childItems = childNodes.stream().map(this::createTreeItem).sorted(treeNodeComparator).toList(); rootItem.getChildren().addAll(childItems); } + + // Get all filters from service + filtersList.addAll(saveAndRestoreService.getAllFilters()); + Platform.runLater(() -> { treeView.setRoot(rootItem); expandNodes(treeView.getRoot()); // Event handler for expanding nodes treeView.getRoot().addEventHandler(TreeItem.branchExpandedEvent(), e -> expandTreeNode(e.getTreeItem())); treeInitializationCountDownLatch.countDown(); + filtersList.add(0, null); + String savedFilterName = getSavedFilterName(); + if (savedFilterName != null) { + Optional f = filtersComboBox.getItems().stream().filter(filter -> filter.getName().equals(savedFilterName)).findFirst(); + f.ifPresent(filter -> filtersComboBox.getSelectionModel().select(filter)); + } + setupFilterActivators(); }); - - loadFilters(); }); } @@ -893,7 +925,7 @@ public void saveLocalState() { findExpandedNodes(expandedNodes, treeView.getRoot()); try { PhoebusPreferenceService.userNodeForClass(SaveAndRestoreApplication.class).put(TREE_STATE, objectMapper.writeValueAsString(expandedNodes)); - if (filterEnabledProperty.get() && filtersComboBox.getSelectionModel().getSelectedItem() != null) { + if (filtersComboBox.getSelectionModel().getSelectedItem() != null) { PhoebusPreferenceService.userNodeForClass(SaveAndRestoreApplication.class).put(FILTER_NAME, objectMapper.writeValueAsString(filtersComboBox.getSelectionModel().getSelectedItem().getName())); } @@ -906,6 +938,7 @@ public void saveLocalState() { public boolean handleTabClosed() { saveLocalState(); webSocketClientService.closeWebSocket(); + filterActivators.forEach(FilterActivator::stop); return true; } @@ -1028,7 +1061,7 @@ protected void moveNodes(List sourceNodes, Node targetNode, TransferMode t * * * @param uri An {@link URI} on the form file:/unique-id?action=[open_sar_node|open_sar_filter]&app=saveandrestore, where unique-id is the - * unique id of a {@link Node} or the unique id (name) of a {@link Filter}. If action is not sepcified, + * unique id of a {@link Node} or the unique id (name) of a {@link Filter}. If action is not specified, * it defaults to open-node. */ public void openResource(URI uri) { @@ -1068,9 +1101,10 @@ public void openResource(URI uri) { * disabled, then all items match as we have a "no filter". */ public boolean matchesFilter(Node node) { - if (!filterEnabledProperty.get()) { + if (currentFilterProperty.isNull().get()) { return true; } + TreeItem selectedItem = treeView.getSelectionModel().getSelectedItem(); if (selectedItem == null) { return searchResultNodes.contains(node); @@ -1080,25 +1114,6 @@ public boolean matchesFilter(Node node) { } } - /** - * Retrieves all {@link Filter}s from service and populates the filter combo box. - */ - private void loadFilters() { - try { - List filters = saveAndRestoreService.getAllFilters(); - Platform.runLater(() -> { - filtersList.setAll(filters); - String savedFilterName = getSavedFilterName(); - if (savedFilterName != null) { - Optional f = filtersComboBox.getItems().stream().filter(filter -> filter.getName().equals(savedFilterName)).findFirst(); - f.ifPresent(filter -> filtersComboBox.getSelectionModel().select(filter)); - } - }); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Failed to load filters", e); - } - } - /** * Applies a {@link Filter} selected by user. The service will be queries for {@link Node}s matching * the {@link Filter}, then the {@link TreeView} is updated based on the search result. @@ -1125,16 +1140,9 @@ private void applyFilter(Filter filter) { }); } - private void filterEnabledChanged(boolean enabled) { - if (!enabled) { - searchResultNodes.clear(); - treeView.refresh(); - } else { - Filter filter = filtersComboBox.getSelectionModel().getSelectedItem(); - if (filter != null) { - applyFilter(filter); - } - } + private void clearFilter() { + searchResultNodes.clear(); + treeView.refresh(); } /** @@ -1163,17 +1171,24 @@ private void filterAddedOrUpdated(Filter filter) { } } + /** + * Handles removal of a {@link Filter}. + *

+ * If the name matches the {@link Filter} currently being loaded, the filter selection is cleared, i.e. + * it switches to "no filter". + *

+ * + * @param name The name of a {@link Filter} + */ private void filterRemoved(String name) { - Optional filterOptional = filtersList.stream().filter(f -> f.getName().equals(name)).findFirst(); + Optional filterOptional = filtersList.stream().filter(f -> f != null && f.getName().equals(name)).findFirst(); if (filterOptional.isPresent()) { Filter filterToRemove = new Filter(); filterToRemove.setName(name); - filtersList.remove(filterToRemove); - // If this is the active filter, unselect it - filterEnabledProperty.set(false); - filtersComboBox.getSelectionModel().select(null); - // And refresh tree view - Platform.runLater(() -> treeView.refresh()); + Platform.runLater(() -> { + filtersList.remove(filterToRemove); + currentFilterProperty.set(null); + }); } } @@ -1423,9 +1438,50 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRestore } } + private void setupFilterActivators() { + filterActivators.addAll( + ServiceLoader.load(FilterActivator.class).stream().map(ServiceLoader.Provider::get).toList()); + filterActivators.forEach(a -> a.setCallbacks(this::activateFilter, this::deactivateFilter)); + } + + private Optional findFilter(String filterName) { + return filtersList.stream().filter(f -> f != null && f.getName().equals(filterName)).findFirst(); + } + + /** + * Activates a {@link Filter}. If filterName does not match any of the available {@link Filter}s, + * nothing happens except logging of the inconsistency. + * + * @param filterName The name of the {@link Filter} to select. + */ + public void activateFilter(String filterName) { + if (autoFilterActive.get()) { + Optional filterOptional = findFilter(filterName); + if (filterOptional.isPresent()) { + Platform.runLater(() -> filtersComboBox.getSelectionModel().select(filterOptional.get())); + } else { + logger.log(Level.WARNING, "Cannot activate filter as filter named \"" + filterName + "\" was not found."); + } + } + } + + /** + * If auto {@link Filter} activation is enabled and the active filter matches filterName, then + * this method will switch to no filter but maintain auto activation. + * @param filterName Name/id of the de-activated filter. + */ + public void deactivateFilter(String filterName) { + if (autoFilterActive.get()) { + Filter currentlySelectedFilter = filtersComboBox.getSelectionModel().getSelectedItem(); + if (currentlySelectedFilter != null && currentlySelectedFilter.getName().equals(filterName)) { + Platform.runLater(() -> filtersComboBox.getSelectionModel().select(null)); + } + } + } + private void handleWebSocketConnected() { serviceConnected.setValue(true); - loadTreeData(); + loadInitialData(); } private void handleWebSocketDisconnected() { 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 6c99126c2f..d3b3c00a64 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 @@ -46,6 +46,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java index 7a048e309b..7001537dde 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterTab.java @@ -38,8 +38,6 @@ public class SearchAndFilterTab extends SaveAndRestoreTab { public static final String SEARCH_AND_FILTER_TAB_ID = "SearchAndFilterTab"; - private SearchAndFilterViewController searchAndFilterViewController; - public SearchAndFilterTab(SaveAndRestoreController saveAndRestoreController) { setId(SEARCH_AND_FILTER_TAB_ID); @@ -67,8 +65,7 @@ public SearchAndFilterTab(SaveAndRestoreController saveAndRestoreController) { Node node = loader.load(); controller = loader.getController(); setContent(node); - searchAndFilterViewController = loader.getController(); - setOnCloseRequest(event -> searchAndFilterViewController.handleSaveAndFilterTabClosed()); + setOnCloseRequest(event -> ((SearchAndFilterViewController)controller).handleSaveAndFilterTabClosed()); } catch (IOException e) { Logger.getLogger(SearchAndFilterTab.class.getName()) .log(Level.SEVERE, "Unable to load search tab content fxml", e); @@ -78,4 +75,12 @@ public SearchAndFilterTab(SaveAndRestoreController saveAndRestoreController) { setText(Messages.search); setGraphic(new ImageView(ImageCache.getImage(ImageCache.class, "/icons/sar-search_18x18.png"))); } + + public void filterActivated(String filterName){ + ((SearchAndFilterViewController)controller).filterActivated(filterName); + } + + public void filterDeactivated(String filterName){ + ((SearchAndFilterViewController)controller).filterDeactivated(filterName); + } } diff --git a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java index 7c30f1f6d9..25f7d53bbe 100644 --- a/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java +++ b/app/save-and-restore/app/src/main/java/org/phoebus/applications/saveandrestore/ui/search/SearchAndFilterViewController.java @@ -647,4 +647,12 @@ public void handleWebSocketMessage(SaveAndRestoreWebSocketMessage saveAndRest case FILTER_REMOVED, FILTER_ADDED_OR_UPDATED -> loadFilters(); } } + + public void filterActivated(String filterName){ + + } + + public void filterDeactivated(String filterName){ + + } } 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 40ab7feaa8..d0ba22cd63 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 @@ -90,6 +90,8 @@ failedGetSpecificFilter=Failed to retrieve filter "{0}" failedSaveFilter=Failed to save filter failedToPasteObjects=Failed to paste object(s) filter=Filter +filterAuto=Auto +filterAutoTooltip=Check to enable automatic filter activation filterEditorDescriptionOrComment=Description/Comment filterEditorNodeName=Node Name filterEditorNodeTypeFolder=Folder @@ -100,7 +102,8 @@ filterEditorTags=Tags filterEditorStartTime=Start Time filterEditorEndTime=End Time filterEditorUser=User -filterName=Filter Name +filterManual=Manual +filterName=Filter Name? filterNotFound=Filter "{0}" not found filterQueryColumn=Query filterLastUpdatedColumn=Last Updated diff --git a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml index 3608b13d1f..531e5fee12 100644 --- a/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml +++ b/app/save-and-restore/app/src/main/resources/org/phoebus/applications/saveandrestore/ui/SaveAndRestoreUI.fxml @@ -2,6 +2,7 @@ + @@ -14,15 +15,11 @@ - - + + @@ -31,13 +28,28 @@ + + + + + + + + diff --git a/app/save-and-restore/app/src/main/resources/save-and-restore-style.css b/app/save-and-restore/app/src/main/resources/save-and-restore-style.css index 019d91e8cc..35f88ee13b 100644 --- a/app/save-and-restore/app/src/main/resources/save-and-restore-style.css +++ b/app/save-and-restore/app/src/main/resources/save-and-restore-style.css @@ -119,12 +119,11 @@ -fx-padding: 0 0 0 2; } -.action-failed-font-size{ - -fx-font-size: 18px; +.combo-box-base:disabled{ + -fx-opacity: 1.0; + -fx-font-weight: bold; } -.action-failed { - -fx-text-fill: #FF0000FF; - -fx-font-weight: bold; - -fx-cursor: hand; +.combo-box > .list-cell:disabled{ + -fx-opacity: 1.0; } \ No newline at end of file diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/search/FilterActivator.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/search/FilterActivator.java new file mode 100644 index 0000000000..fdc4099e6b --- /dev/null +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/search/FilterActivator.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2025 European Spallation Source ERIC. + */ + +package org.phoebus.applications.saveandrestore.model.search; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; + +/** + * A {@link FilterActivator} is a class activating or de-activating a {@link Filter}. + * When the business logic evaluates to activation of a {@link Filter}, + * a registered callback is invoked. Conversely, when the business logic no longer evaluates to activation of a + * {@link Filter}, a registered callback is invoked. + * + *

+ * NOTE: Implementations must make sure to not select {@link Filter}s + * at the same time. Only one implementation at a given point in time should activate a particular {@link Filter}. + *

+ * + *

Example: based on one or several PV values a {@link FilterActivator} implementation may set up conditions to perform + * a activation/de-activation of {@link Filter}s.

+ */ +public abstract class FilterActivator { + + private Consumer filterActivatedCallback; + private Consumer filterDeactivatedCallback; + protected String activatedFilter; + /** + * + * @return A list of {@link Filter} names supported by + * all {@link FilterActivator} implementations. + */ + public List getSupportedFilterNames() { + return Collections.emptyList(); + } + + /** + * Registers the callbacks invoked when a {@link Filter} is + * activated or de-activated. Calls the {@link #start()} method to ensure that any initial + * state in the implementation is not set before the callbacks have been registered. + * @param filterActivatedCallback The callback method called when filter is activated. + * @param filterDeactivatedCallback The callback method called when filter is de-activated. + */ + public void setCallbacks(Consumer filterActivatedCallback, Consumer filterDeactivatedCallback) { + this.filterActivatedCallback = filterActivatedCallback; + this.filterDeactivatedCallback = filterDeactivatedCallback; + start(); + } + /** + * Invokes callback when a {@link Filter} is activated. + * @param filterName Unique name of a {@link Filter} + */ + public void filterActivated(String filterName) { + activatedFilter = filterName; + if (filterActivatedCallback != null) { + filterActivatedCallback.accept(filterName); + } + } + + /** + * Invokes callback when a {@link Filter} is de-activated. + * @param filterName Unique name of a {@link Filter} + */ + public void filterDeactivated(String filterName) { + activatedFilter = null; + if (filterDeactivatedCallback != null) { + filterDeactivatedCallback.accept(filterName); + } + } + + /** + * + * @return Name of a {@link Filter} if activated by any + * implementation, otherwise null. + */ + public String getActivatedFilter(){ + return activatedFilter; + } + + /** + * Implementations must make sure this method starts whatever needed to deliver both + * initial state and any subsequent updates. + */ + public abstract void start(); + + /** + * Implementations should override this to perform cleanup. + */ + public abstract void stop(); +} diff --git a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java index 3e636cbf5a..42c4807a74 100644 --- a/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java +++ b/app/save-and-restore/model/src/main/java/org/phoebus/applications/saveandrestore/model/websocket/WebMessageDeserializer.java @@ -33,8 +33,8 @@ public WebMessageDeserializer(Class clazz) { * @return A {@link SaveAndRestoreWebSocketMessage} object, or null if deserialization fails. */ @Override - public SaveAndRestoreWebSocketMessage deserialize(JsonParser jsonParser, - DeserializationContext context) { + public SaveAndRestoreWebSocketMessage deserialize(JsonParser jsonParser, + DeserializationContext context) { try { JsonNode rootNode = jsonParser.getCodec().readTree(jsonParser); String messageType = rootNode.get("messageType").asText(); diff --git a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java index 57e34face3..d06e120eef 100644 --- a/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java +++ b/services/save-and-restore/src/main/java/org/phoebus/service/saveandrestore/web/controllers/FilterController.java @@ -26,11 +26,15 @@ import org.phoebus.service.saveandrestore.websocket.WebSocketHandler; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; import java.security.Principal; import java.util.List; -import java.util.Optional; /** * Controller class for {@link Filter} endpoints. @@ -64,7 +68,6 @@ public Filter saveFilter(@RequestBody final Filter filter, } /** - * * @return A {@link List} of all persisted {@link Filter} objects. Empty if none are found. */ @SuppressWarnings("unused") @@ -75,7 +78,8 @@ public List getAllFilters() { /** * Deletes a {@link Filter} - * @param name Unique name of the {@link Filter} + * + * @param name Unique name of the {@link Filter} * @param principal User {@link Principal} as injected by Spring. */ @SuppressWarnings("unused")