diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java index 81abb57115..a127982ca6 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogClient.java @@ -20,6 +20,7 @@ import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; import org.phoebus.logbook.LogEntryChangeHandler; +import org.phoebus.logbook.LogTemplate; import org.phoebus.logbook.Logbook; import org.phoebus.logbook.LogbookException; import org.phoebus.logbook.Messages; @@ -43,6 +44,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.net.http.HttpHeaders; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -562,4 +564,36 @@ public SearchResult getArchivedEntries(long id){ throw new RuntimeException(e); } } + + @Override + public Collection getTemplates(){ + try { + return OlogObjectMappers.logEntryDeserializer.readValue( + service.path("templates").accept(MediaType.APPLICATION_JSON).get(String.class), + new TypeReference>() { + }); + } catch (UniformInterfaceException | ClientHandlerException | IOException e) { + logger.log(Level.WARNING, "Unable to get templates from service", e); + return Collections.emptySet(); + } + } + + @Override + public LogTemplate saveTemplate(LogTemplate template) throws LogbookException{ + ClientResponse clientResponse = service.path("templates").accept(MediaType.APPLICATION_JSON_TYPE) + .header("Content-Type", MediaType.APPLICATION_JSON_TYPE) + .put(ClientResponse.class, template); + if (clientResponse.getStatus() > 300) { + logger.log(Level.SEVERE, "Failed to create template: " + clientResponse); + throw new LogbookException(clientResponse.toString()); + } + + try { + return OlogObjectMappers.logEntryDeserializer.readValue(clientResponse.getEntityInputStream(), LogTemplate.class); + } catch (IOException e) { + logger.log(Level.SEVERE, "Failed to submit template, got client exception", e); + throw new LogbookException(e); + } + + } } diff --git a/app/logbook/olog/ui/pom.xml b/app/logbook/olog/ui/pom.xml index 9df3fa0dc3..8103b68f94 100644 --- a/app/logbook/olog/ui/pom.xml +++ b/app/logbook/olog/ui/pom.xml @@ -40,6 +40,11 @@ core-security 4.7.4-SNAPSHOT + + org.phoebus + core-ui + 4.7.4-SNAPSHOT + org.jfxtras jfxtras-agenda diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java index a06cafcd16..5be01d52e3 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryDisplayController.java @@ -36,6 +36,7 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import org.phoebus.logbook.LogEntry; +import org.phoebus.logbook.olog.ui.write.EditMode; import org.phoebus.logbook.olog.ui.write.LogEntryEditorStage; import org.phoebus.olog.es.api.model.LogGroupProperty; import org.phoebus.olog.es.api.model.OlogLog; @@ -180,13 +181,13 @@ public void jumpToLogEntry() { public void reply() { // Show a new editor dialog. When user selects to save the reply entry, update the original log entry // to ensure that it contains the log group property. - new LogEntryEditorStage(new OlogLog(), logEntryProperty.get(), null).show(); + new LogEntryEditorStage(new OlogLog(), logEntryProperty.get(), EditMode.NEW_LOG_ENTRY).show(); } @FXML public void newLogEntry(){ // Show a new editor dialog. - new LogEntryEditorStage(new OlogLog(), null, null).show(); + new LogEntryEditorStage(new OlogLog(), null, EditMode.NEW_LOG_ENTRY).show(); } @FXML diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java index e24b3edd71..4ba91940d5 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/LogEntryTableViewController.java @@ -48,9 +48,9 @@ import org.phoebus.logbook.SearchResult; import org.phoebus.logbook.olog.ui.query.OlogQuery; import org.phoebus.logbook.olog.ui.query.OlogQueryManager; +import org.phoebus.logbook.olog.ui.write.EditMode; import org.phoebus.logbook.olog.ui.spi.Decoration; import org.phoebus.logbook.olog.ui.write.LogEntryEditorStage; -import org.phoebus.logbook.olog.ui.write.LogEntryUpdateStage; import org.phoebus.olog.es.api.model.LogGroupProperty; import org.phoebus.olog.es.api.model.OlogLog; import org.phoebus.security.store.SecureStore; @@ -186,12 +186,12 @@ public void initialize() { MenuItem menuItemNewLogEntry = new MenuItem(Messages.NewLogEntry); menuItemNewLogEntry.acceleratorProperty().setValue(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN)); - menuItemNewLogEntry.setOnAction(ae -> new LogEntryEditorStage(new OlogLog(), null, null).show()); + menuItemNewLogEntry.setOnAction(ae -> new LogEntryEditorStage(new OlogLog(), null, EditMode.NEW_LOG_ENTRY).showAndWait()); MenuItem menuItemUpdateLogEntry = new MenuItem(Messages.UpdateLogEntry); menuItemUpdateLogEntry.visibleProperty().bind(Bindings.createBooleanBinding(() -> selectedLogEntries.size() == 1, selectedLogEntries)); menuItemUpdateLogEntry.acceleratorProperty().setValue(new KeyCodeCombination(KeyCode.U, KeyCombination.CONTROL_DOWN)); - menuItemUpdateLogEntry.setOnAction(ae -> new LogEntryUpdateStage(selectedLogEntries.get(0), null).show()); + menuItemUpdateLogEntry.setOnAction(ae -> new LogEntryEditorStage(selectedLogEntries.get(0), null, EditMode.UPDATE_LOG_ENTRY).show()); contextMenu.getItems().addAll(groupSelectedEntries, menuItemShowHideAll, menuItemNewLogEntry); if (LogbookUIPreferences.log_entry_update_support) { diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java index 7d5d280eaf..bc56349035 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/Messages.java @@ -23,6 +23,7 @@ public class Messages AttachmentsDirectoryNotWritable, AttachmentsFileNotDirectory, AttachmentsNoStorage, + AvailableTemplates, Back, CloseRequestHeader, CloseRequestButtonContinue, diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/AttachmentsEditorController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/AttachmentsEditorController.java index 51beabf177..b8e95a4e78 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/AttachmentsEditorController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/AttachmentsEditorController.java @@ -23,6 +23,8 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleStringProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.collections.ListChangeListener; import javafx.embed.swing.SwingFXUtils; import javafx.fxml.FXML; @@ -117,26 +119,31 @@ public class AttachmentsEditorController { private final SimpleStringProperty sizeLimitsText = new SimpleStringProperty(); /** - * @param logEntry The log entry template potentially holding a set of attachments. Note + * @param logEntry The log entry potentially holding a set of attachments. Note * that files associated with these attachments are considered temporary and * are subject to removal when the log entry has been committed. */ public AttachmentsEditorController(LogEntry logEntry) { this.logEntry = logEntry; - // If the log entry has an attachment - e.g. log entry created from display or data browser - - // then add the file size to the total attachments size - Collection attachments = logEntry.getAttachments(); - if (attachments != null && !attachments.isEmpty()) { - attachments.forEach(a -> attachedFilesSize += getFileSize(a.getFile())); - } } @FXML public void initialize() { - attachmentsViewController.setAttachments(logEntry.getAttachments()); + // If the log entry has an attachment - e.g. log entry created from display or data browser - + // then add the file size to the total attachments size. + Collection attachments = logEntry.getAttachments(); + if (attachments != null && !attachments.isEmpty()) { + attachments.forEach(a -> { + if(a.getFile() != null){ + attachedFilesSize += getFileSize(a.getFile()); + } + }); + } + + attachmentsViewController.setAttachments(attachments); - filesToDeleteAfterSubmit.addAll(logEntry.getAttachments().stream().map(Attachment::getFile).toList()); + filesToDeleteAfterSubmit.addAll(attachments.stream().map(Attachment::getFile).toList()); removeButton.setGraphic(ImageCache.getImageView(ImageCache.class, "/icons/delete.png")); removeButton.disableProperty().bind(Bindings.isEmpty(attachmentsViewController.getSelectedAttachments())); @@ -335,10 +342,19 @@ private void addImage(Image image, String id) { } } - public List getAttachments() { + /** + * + * @return The {@link ObservableList} of {@link Attachment}s managed in the {@link AttachmentsViewController}. + */ + public ObservableList getAttachments() { return attachmentsViewController.getAttachments(); } + /** + * Sets the file upload constraints information in the editor. + * @param maxFileSize Maximum size for a single file. + * @param maxRequestSize Maximum total size of all attachments. + */ public void setSizeLimits(String maxFileSize, String maxRequestSize) { this.maxFileSize = Double.parseDouble(maxFileSize); this.maxRequestSize = Double.parseDouble(maxRequestSize); @@ -380,4 +396,11 @@ private void showFileSizeExceedsLimit(File file) { private void showTotalSizeExceedsLimit() { Platform.runLater(() -> sizesErrorMessage.set(Messages.RequestTooLarge)); } + + /** + * Clears list of {@link Attachment}s. + */ + public void clearAttachments(){ + getAttachments().clear(); + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/ContextMenuLogging.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/ContextMenuLogging.java index 8907ca28e1..0086e44e8b 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/ContextMenuLogging.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/ContextMenuLogging.java @@ -44,7 +44,7 @@ public void call(Node parent, Selection selection) { adaptedSelections.add(adapted); }); }); - new LogEntryEditorStage(adaptedSelections.get(0), null, null).show(); + new LogEntryEditorStage(adaptedSelections.get(0), null, EditMode.NEW_LOG_ENTRY).show(); } @Override diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/EditMode.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/EditMode.java new file mode 100644 index 0000000000..aac9c57b90 --- /dev/null +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/EditMode.java @@ -0,0 +1,11 @@ +/* + * Copyright (C) 2024 European Spallation Source ERIC. + */ + +package org.phoebus.logbook.olog.ui.write; + +public enum EditMode { + + NEW_LOG_ENTRY, + UPDATE_LOG_ENTRY +} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryCompletionHandler.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryCompletionHandler.java deleted file mode 100644 index 897ab97ff1..0000000000 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryCompletionHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2019 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.logbook.olog.ui.write; - -import org.phoebus.logbook.LogEntry; - -public interface LogEntryCompletionHandler { - - public void handleResult(LogEntry logEntry); -} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java index 365537a9d1..7b3e59a63a 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorController.java @@ -25,24 +25,53 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.geometry.Side; -import javafx.scene.control.*; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.CustomMenuItem; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.MenuItem; +import javafx.scene.control.PasswordField; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.ToggleButton; import javafx.scene.image.Image; import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; +import javafx.util.Callback; +import javafx.util.StringConverter; +import org.phoebus.framework.autocomplete.Proposal; +import org.phoebus.framework.autocomplete.ProposalProvider; +import org.phoebus.framework.autocomplete.ProposalService; import org.phoebus.framework.jobs.JobManager; import org.phoebus.framework.selection.SelectionService; -import org.phoebus.logbook.*; +import org.phoebus.logbook.LogClient; +import org.phoebus.logbook.LogEntry; +import org.phoebus.logbook.LogFactory; +import org.phoebus.logbook.LogService; +import org.phoebus.logbook.LogTemplate; +import org.phoebus.logbook.Logbook; +import org.phoebus.logbook.LogbookException; +import org.phoebus.logbook.LogbookPreferences; +import org.phoebus.logbook.Tag; import org.phoebus.logbook.olog.ui.HelpViewer; import org.phoebus.logbook.olog.ui.LogbookUIPreferences; +import org.phoebus.logbook.olog.ui.Messages; import org.phoebus.logbook.olog.ui.PreviewViewer; -import org.phoebus.logbook.olog.ui.menu.SendToLogBookApp; import org.phoebus.olog.es.api.OlogProperties; import org.phoebus.olog.es.api.model.OlogLog; import org.phoebus.security.store.SecureStore; @@ -50,12 +79,18 @@ import org.phoebus.security.tokens.ScopedAuthenticationToken; import org.phoebus.security.tokens.SimpleAuthenticationToken; import org.phoebus.ui.Preferences; +import org.phoebus.ui.autocomplete.AutocompleteMenu; import org.phoebus.ui.dialog.ListSelectionPopOver; import org.phoebus.ui.javafx.ImageCache; import org.phoebus.util.time.TimestampFormats; import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -66,21 +101,25 @@ */ public class LogEntryEditorController { - private final LogEntryCompletionHandler completionHandler; - private final Logger logger = Logger.getLogger(LogEntryEditorController.class.getName()); @FXML + @SuppressWarnings("unused") private VBox editorPane; @FXML + @SuppressWarnings("unused") private VBox errorPane; @FXML + @SuppressWarnings("unused") private Button submitButton; @FXML + @SuppressWarnings("unused") private Button cancelButton; @FXML + @SuppressWarnings("unused") private ProgressIndicator progressIndicator; @FXML + @SuppressWarnings("unused") private Label completionMessageLabel; @SuppressWarnings("unused") @@ -91,39 +130,66 @@ public class LogEntryEditorController { private LogPropertiesEditorController logPropertiesEditorController; @FXML + @SuppressWarnings("unused") private Label userFieldLabel; @FXML + @SuppressWarnings("unused") private Label passwordFieldLabel; @FXML + @SuppressWarnings("unused") private TextField userField; @FXML + @SuppressWarnings("unused") + private TextField dateField; + @FXML + @SuppressWarnings("unused") private PasswordField passwordField; @FXML + @SuppressWarnings("unused") private Label levelLabel; @FXML - private TextField dateField; - @FXML + @SuppressWarnings("unused") private ComboBox levelSelector; @FXML + @SuppressWarnings("unused") private Label titleLabel; @FXML + @SuppressWarnings("unused") private TextField titleField; @FXML + @SuppressWarnings("unused") private TextArea textArea; @FXML + @SuppressWarnings("unused") private Button addLogbooks; @FXML + @SuppressWarnings("unused") private Button addTags; @FXML + @SuppressWarnings("unused") private ToggleButton logbooksDropdownButton; @FXML + @SuppressWarnings("unused") private ToggleButton tagsDropdownButton; @FXML + @SuppressWarnings("unused") private Label logbooksLabel; @FXML + @SuppressWarnings("unused") private TextField logbooksSelection; @FXML + @SuppressWarnings("unused") private TextField tagsSelection; + @FXML + @SuppressWarnings("unused") + private HBox templateControls; + @FXML + @SuppressWarnings("unused") + private ComboBox templateSelector; + + @FXML + @SuppressWarnings("unused") + private Node attachmentsPane; private final ContextMenu logbookDropDown = new ContextMenu(); private final ContextMenu tagDropDown = new ContextMenu(); @@ -170,15 +236,20 @@ public class LogEntryEditorController { */ private String originalTitle = ""; + private final EditMode editMode; + /** - * Version of remote service + * Result of a submission, caller may use this to take further action once a {@link LogEntry} has been created. */ - private String serverVersion; + private Optional logEntryResult = Optional.empty(); - public LogEntryEditorController(LogEntry logEntry, LogEntry inReplyTo, LogEntryCompletionHandler logEntryCompletionHandler) { + private final ObservableList templatesProperty = + FXCollections.observableArrayList(); + + public LogEntryEditorController(LogEntry logEntry, LogEntry inReplyTo, EditMode editMode) { this.replyTo = inReplyTo; - this.completionHandler = logEntryCompletionHandler; this.logFactory = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory); + this.editMode = editMode; updateCredentialsProperty = updateCredentials; // This is the reply case: @@ -199,11 +270,6 @@ public LogEntryEditorController(LogEntry logEntry, LogEntry inReplyTo, LogEntryC @FXML public void initialize() { - // This could be configured in the fxml, but then these UI components would not be visible - // in Scene Builder. - completionMessageLabel.textProperty().set(""); - progressIndicator.visibleProperty().bind(submissionInProgress); - // Remote log service not reachable, so show error pane. if (!checkConnectivity()) { errorPane.visibleProperty().set(true); @@ -211,6 +277,18 @@ public void initialize() { return; } + templateControls.managedProperty().bind(templateControls.visibleProperty()); + templateControls.visibleProperty().setValue(editMode.equals(EditMode.NEW_LOG_ENTRY)); + + attachmentsPane.managedProperty().bind(attachmentsPane.visibleProperty()); + attachmentsPane.visibleProperty().setValue(editMode.equals(EditMode.NEW_LOG_ENTRY)); + + // This could be configured in the fxml, but then these UI components would not be visible + // in Scene Builder. + completionMessageLabel.textProperty().set(""); + progressIndicator.visibleProperty().bind(submissionInProgress); + + submitButton.managedProperty().bind(submitButton.visibleProperty()); submitButton.disableProperty().bind(Bindings.createBooleanBinding(() -> !inputValid.get() || submissionInProgress.get(), inputValid, submissionInProgress)); @@ -272,18 +350,18 @@ public void initialize() { selectedLevelProperty.set(logEntry.getLevel() != null ? logEntry.getLevel() : availableLevels.get(0)); levelSelector.getSelectionModel().select(selectedLevelProperty.get()); - dateField.setText(TimestampFormats.DATE_FORMAT.format(Instant.now())); + titleField.textProperty().bindBidirectional(titleProperty); titleProperty.addListener((changeListener, oldVal, newVal) -> { - if (newVal.trim().isEmpty()) { + if (newVal == null || newVal.trim().isEmpty()) { titleLabel.setTextFill(Color.RED); } else { titleLabel.setTextFill(Color.BLACK); } - if (!newVal.equals(originalTitle)) { + if (newVal != null && !newVal.equals(originalTitle)) { isDirty = true; } }); @@ -368,17 +446,111 @@ public void initialize() { ); selectedTags.addListener((ListChangeListener) change -> { + if (change.getList() == null) { + return; + } List newSelection = new ArrayList<>(change.getList()); tagsPopOver.setAvailable(availableTagsAsStringList, newSelection); tagsPopOver.setSelected(newSelection); + newSelection.forEach(t -> updateDropDown(tagDropDown, t, true)); }); selectedLogbooks.addListener((ListChangeListener) change -> { + if (change.getList() == null) { + return; + } List newSelection = new ArrayList<>(change.getList()); logbooksPopOver.setAvailable(availableLogbooksAsStringList, newSelection); logbooksPopOver.setSelected(newSelection); + newSelection.forEach(l -> updateDropDown(logbookDropDown, l, true)); + }); + + AutocompleteMenu autocompleteMenu = new AutocompleteMenu(new ProposalService(new ProposalProvider() { + @Override + public String getName() { + return Messages.AvailableTemplates; + } + + @Override + public List lookup(String text) { + List proposals = new ArrayList<>(); + templatesProperty.forEach(template -> { + if (template.name().contains(text)) { + proposals.add(new Proposal(template.name())); + } + }); + return proposals; + } + })); + + autocompleteMenu.attachField(templateSelector.getEditor()); + + templateSelector.setCellFactory(new Callback<>() { + @Override + public ListCell call(ListView logTemplateListView) { + return new ListCell<>() { + @Override + protected void updateItem(LogTemplate item, boolean empty) { + super.updateItem(item, empty); + if (item == null || empty) { + setGraphic(null); + } else { + setGraphic(new Label(item.name())); + } + } + }; + } + }); + + templateSelector.valueProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + templateSelector.getEditor().textProperty().set(newValue.name()); + if (!newValue.equals(oldValue)) { + loadTemplate(newValue); + } + } else { + // E.g. if user specifies an invalid template name + loadTemplate(null); + } }); + // Hide autocomplete menu when user clicks drop-down button + templateSelector.showingProperty().addListener((obs, wasShowing, isNowShowing) -> autocompleteMenu.hide()); + + templateSelector.getEditor().textProperty().addListener((obs, o, n) -> { + if (n != null && !n.isEmpty()) { + Optional logTemplate = + templatesProperty.stream().filter(t -> t.name().equals(n)).findFirst(); + logTemplate.ifPresent(template -> templateSelector.valueProperty().setValue(template)); + } + }); + + templateSelector.setConverter( + new StringConverter<>() { + @Override + public String toString(LogTemplate template) { + if (template == null) { + return null; + } else { + return template.name(); + } + } + + /** + * Converts the user specified string to a {@link LogTemplate} + * @param name The name of an (existing) template + * @return A {@link LogTemplate} if name matches an existing one, otherwise null + */ + @Override + public LogTemplate fromString(String name) { + Optional logTemplate = + templatesProperty.stream().filter(t -> t.name().equals(name)).findFirst(); + return logTemplate.orElse(null); + } + }); + + templateSelector.itemsProperty().bind(new SimpleObjectProperty<>(templatesProperty)); + // Note: logbooks and tags are retrieved asynchronously from service getServerSideStaticData(); } @@ -396,6 +568,7 @@ public void cancel() { } @FXML + @SuppressWarnings("unused") public void showHelp() { new HelpViewer(LogbookUIPreferences.markup_help).show(); } @@ -404,12 +577,14 @@ public void showHelp() { * Handler for HTML preview button */ @FXML + @SuppressWarnings("unused") public void showHtmlPreview() { new PreviewViewer(getDescription(), attachmentsEditorController.getAttachments()).show(); } @FXML + @SuppressWarnings("unused") public void submit() { submissionInProgress.set(true); @@ -421,40 +596,38 @@ public void submit() { ologLog.setLevel(selectedLevelProperty.get()); ologLog.setLogbooks(getSelectedLogbooks()); ologLog.setTags(getSelectedTags()); - ologLog.setAttachments(attachmentsEditorController.getAttachments()); + if (editMode.equals(EditMode.NEW_LOG_ENTRY)) { + ologLog.setAttachments(attachmentsEditorController.getAttachments()); + } ologLog.setProperties(logPropertiesEditorController.getProperties()); LogClient logClient = logFactory.getLogClient(new SimpleAuthenticationToken(usernameProperty.get(), passwordProperty.get())); - LogEntry result; try { if (replyTo == null) { - result = logClient.set(ologLog); + logEntryResult = Optional.of(logClient.set(ologLog)); } else { - result = logClient.reply(ologLog, replyTo); + logEntryResult = Optional.of(logClient.reply(ologLog, replyTo)); } - // Not dirty any more... + // Not dirty anymore... isDirty = false; - if (result != null) { - if (completionHandler != null) { - completionHandler.handleResult(result); - } - // Set username and password in secure store if submission of log entry completes successfully - if (Preferences.save_credentials) { - // Get the SecureStore. Store username and password. - try { - SecureStore store = new SecureStore(); - ScopedAuthenticationToken scopedAuthenticationToken = - new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, usernameProperty.get(), passwordProperty.get()); - store.setScopedAuthentication(scopedAuthenticationToken); - } catch (Exception ex) { - logger.log(Level.WARNING, "Secure Store file not found.", ex); - } + + // Set username and password in secure store if submission of log entry completes successfully + if (Preferences.save_credentials) { + // Get the SecureStore. Store username and password. + try { + SecureStore store = new SecureStore(); + ScopedAuthenticationToken scopedAuthenticationToken = + new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, usernameProperty.get(), passwordProperty.get()); + store.setScopedAuthentication(scopedAuthenticationToken); + } catch (Exception ex) { + logger.log(Level.WARNING, "Secure Store file not found.", ex); } - attachmentsEditorController.deleteTemporaryFiles(); - // This will close the editor - Platform.runLater(this::cancel); } + attachmentsEditorController.deleteTemporaryFiles(); + // This will close the editor + Platform.runLater(this::cancel); + } catch (LogbookException e) { logger.log(Level.WARNING, "Unable to submit log entry", e); Platform.runLater(() -> { @@ -472,6 +645,7 @@ public void submit() { } @FXML + @SuppressWarnings("unused") public void setLevel() { selectedLevelProperty.set(levelSelector.getSelectionModel().getSelectedItem()); } @@ -485,6 +659,7 @@ public String getDescription() { } @FXML + @SuppressWarnings("unused") public void addLogbooks() { logbooksPopOver.show(addLogbooks); } @@ -506,15 +681,16 @@ private void setSelectedLogbooks(List proposedLogbooks, List exi private void setSelected(List proposed, List existing, Consumer addFunction, Consumer removeFunction) { List addedTags = proposed.stream() .filter(tag -> !existing.contains(tag)) - .collect(Collectors.toList()); + .toList(); List removedTags = existing.stream() .filter(tag -> !proposed.contains(tag)) - .collect(Collectors.toList()); + .toList(); addedTags.forEach(addFunction); removedTags.forEach(removeFunction); } @FXML + @SuppressWarnings("unused") public void selectLogbooks() { if (logbooksDropdownButton.isSelected()) { logbookDropDown.show(logbooksSelection, Side.BOTTOM, 0, 0); @@ -524,6 +700,7 @@ public void selectLogbooks() { } @FXML + @SuppressWarnings("unused") public void addTags() { tagsPopOver.show(addTags); } @@ -543,6 +720,7 @@ private void setSelectedTags(List proposedTags, List existingTag } @FXML + @SuppressWarnings("unused") public void selectTags() { if (tagsDropdownButton.isSelected()) { tagDropDown.show(tagsSelection, Side.BOTTOM, 0, 0); @@ -575,7 +753,7 @@ private void getServerSideStaticData() { Collections.sort(availableLogbooksAsStringList); List preSelectedLogbooks = - logEntry.getLogbooks().stream().map(Logbook::getName).collect(Collectors.toList()); + logEntry.getLogbooks().stream().map(Logbook::getName).toList(); List defaultLogbooks = Arrays.asList(LogbookUIPreferences.default_logbooks); availableLogbooksAsStringList.forEach(logbook -> { CheckBox checkBox = new CheckBox(logbook); @@ -606,7 +784,7 @@ private void getServerSideStaticData() { Collections.sort(availableLogbooksAsStringList); List preSelectedTags = - logEntry.getTags().stream().map(Tag::getName).collect(Collectors.toList()); + logEntry.getTags().stream().map(Tag::getName).toList(); availableTagsAsStringList.forEach(tag -> { CheckBox checkBox = new CheckBox(tag); CustomMenuItem newTag = new CustomMenuItem(checkBox); @@ -636,12 +814,13 @@ private void getServerSideStaticData() { ObjectMapper objectMapper = new ObjectMapper(); try { JsonNode jsonNode = objectMapper.readTree(serverInfo); - serverVersion = jsonNode.get("version").asText(); attachmentsEditorController.setSizeLimits(jsonNode.get("serverConfig").get("maxFileSize").asText(), jsonNode.get("serverConfig").get("maxRequestSize").asText()); } catch (Exception e) { logger.log(Level.WARNING, "Failed to get or parse response from server info request", e); } + + templatesProperty.setAll(logClient.getTemplates().stream().toList()); }); } @@ -701,8 +880,41 @@ private boolean checkConnectivity() { logClient.serviceInfo(); return true; } catch (Exception e) { - Logger.getLogger(SendToLogBookApp.class.getName()).warning("Failed to query logbook service, it may be off-line."); + Logger.getLogger(LogEntryEditorController.class.getName()).warning("Failed to query logbook service, it may be off-line."); return false; } } + + public Optional getLogEntryResult() { + return logEntryResult; + } + + /** + * Loads template to configure UI elements. + * + * @param logTemplate A {@link LogTemplate} selected by user. If null, all log entry elements + * will be cleared, except the Level selector, which will be set to the top-most item. + */ + private void loadTemplate(LogTemplate logTemplate) { + if (logTemplate != null) { + titleProperty.set(logTemplate.title()); + descriptionProperty.set(logTemplate.source()); + logPropertiesEditorController.setProperties(logTemplate.properties()); + selectedTags.setAll(logTemplate.tags().stream().map(Tag::getName).toList()); + selectedLogbooks.setAll(logTemplate.logbooks().stream().map(Logbook::getName).toList()); + levelSelector.getSelectionModel().select(logTemplate.level()); + selectedTags.forEach(t -> updateDropDown(tagDropDown, t, true)); + selectedLogbooks.forEach(l -> updateDropDown(logbookDropDown, l, true)); + } else { + titleProperty.set(null); + descriptionProperty.set(null); + logPropertiesEditorController.clearSelectedProperties(); + attachmentsEditorController.clearAttachments(); + selectedTags.setAll(Collections.emptyList()); + selectedLogbooks.setAll(Collections.emptyList()); + levelSelector.getSelectionModel().select(0); + selectedTags.forEach(t -> updateDropDown(tagDropDown, t, false)); + selectedLogbooks.forEach(l -> updateDropDown(logbookDropDown, l, false)); + } + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorStage.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorStage.java index 2911280d1b..6a208d674c 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorStage.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryEditorStage.java @@ -35,29 +35,36 @@ import org.phoebus.ui.dialog.DialogHelper; import java.util.Collection; +import java.util.Optional; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; +/** + * {@link Stage} subclass rendering a UI for the purpose of editing/submitting a {@link LogEntry}. + * Callers that need to handle the outcome (i.e. when the {@link Stage} is closed) should + * call first call {@link #showAndWait()} and then get a potential result using {@link #getLogEntryResult()}. + */ public class LogEntryEditorStage extends Stage { private LogEntryEditorController logEntryEditorController; /** * A stand-alone window containing components needed to create a logbook entry. * - * @param logEntry Pre-populated data for the log entry, e.g. date and (optionally) screen shot. + * @param logEntry Pre-populated data for the log entry, e.g. date and (optionally) screenshot. */ public LogEntryEditorStage(LogEntry logEntry) { - this(logEntry, null, null); + this(logEntry, null, EditMode.NEW_LOG_ENTRY); } /** * A stand-alone window containing components needed to create a logbook entry. * - * @param logEntry Pre-populated data for the log entry, e.g. date and (optionally) screen shot. - * @param completionHandler A completion handler called when service call completes. + * @param logEntry Pre-populated data for the log entry, e.g. date and (optionally) screenshot. + * @param replyTo Existing {@link LogEntry} for which the new {@link LogEntry} is a reply. If null, + * then it is assumed this is invoked to not crate a reply. */ - public LogEntryEditorStage(LogEntry logEntry, LogEntry replyTo, LogEntryCompletionHandler completionHandler) { + public LogEntryEditorStage(LogEntry logEntry, LogEntry replyTo, EditMode editMode) { initModality(Modality.WINDOW_MODAL); ResourceBundle resourceBundle = NLS.getMessages(Messages.class); @@ -66,8 +73,8 @@ public LogEntryEditorStage(LogEntry logEntry, LogEntry replyTo, LogEntryCompleti fxmlLoader.setControllerFactory(clazz -> { try { if (clazz.isAssignableFrom(LogEntryEditorController.class)) { - logEntryEditorController = (LogEntryEditorController) clazz.getConstructor(LogEntry.class, LogEntry.class, LogEntryCompletionHandler.class) - .newInstance(logEntry, replyTo, completionHandler); + logEntryEditorController = (LogEntryEditorController) clazz.getConstructor(LogEntry.class, LogEntry.class, EditMode.class) + .newInstance(logEntry, replyTo, editMode); return logEntryEditorController; } else if (clazz.isAssignableFrom(AttachmentsEditorController.class)) { return clazz.getConstructor(LogEntry.class).newInstance(logEntry); @@ -95,11 +102,10 @@ public LogEntryEditorStage(LogEntry logEntry, LogEntry replyTo, LogEntryCompleti we.consume(); handleCloseEditor(logEntryEditorController.isDirty(), fxmlLoader.getRoot()); }); - if (replyTo == null) { - setTitle(Messages.NewLogEntry); - } - else { - setTitle(Messages.ReplyToLogEntry); + + switch (editMode){ + case NEW_LOG_ENTRY -> setTitle(replyTo == null ? Messages.NewLogEntry : Messages.EditLogEntry); + case UPDATE_LOG_ENTRY -> setTitle(Messages.EditLogEntry); } } @@ -126,4 +132,13 @@ public void handleCloseEditor(boolean entryIsDirty, Node parent) { close(); } } + + /** + * + * @return A potentially empty result of {@link LogEntry} submission. + */ + @SuppressWarnings("unused") + public Optional getLogEntryResult(){ + return logEntryEditorController.getLogEntryResult(); + } } diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java deleted file mode 100644 index dfb9c0727a..0000000000 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateController.java +++ /dev/null @@ -1,715 +0,0 @@ -/* - * Copyright (C) 2022 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.logbook.olog.ui.write; - -import javafx.application.Platform; -import javafx.beans.binding.Bindings; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.fxml.FXML; -import javafx.geometry.Side; -import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.VBox; -import javafx.scene.paint.Color; -import org.phoebus.framework.jobs.JobManager; -import org.phoebus.framework.selection.SelectionService; -import org.phoebus.logbook.*; -import org.phoebus.logbook.olog.ui.*; -import org.phoebus.logbook.olog.ui.menu.SendToLogBookApp; -import org.phoebus.olog.es.api.OlogProperties; -import org.phoebus.olog.es.api.model.OlogAttachment; -import org.phoebus.olog.es.api.model.OlogLog; -import org.phoebus.security.store.SecureStore; -import org.phoebus.security.tokens.AuthenticationScope; -import org.phoebus.security.tokens.ScopedAuthenticationToken; -import org.phoebus.security.tokens.SimpleAuthenticationToken; -import org.phoebus.ui.Preferences; -import org.phoebus.ui.dialog.ListSelectionPopOver; -import org.phoebus.ui.javafx.ImageCache; -import org.phoebus.util.time.TimestampFormats; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.time.Instant; -import java.util.*; -import java.util.function.Consumer; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * Controller for the {@link LogEntryUpdateStage}. - */ -public class LogEntryUpdateController { - - private final LogEntryCompletionHandler completionHandler; - - private final Logger logger = Logger.getLogger(LogEntryUpdateController.class.getName()); - - @FXML - private VBox editorPane; - @FXML - private VBox errorPane; - @FXML - private Button submitButton; - @FXML - private Button cancelButton; - @FXML - private ProgressIndicator progressIndicator; - @FXML - private Label completionMessageLabel; - - @SuppressWarnings("unused") - @FXML - private AttachmentsViewController attachmentsViewController; - @SuppressWarnings("unused") - @FXML - private LogPropertiesEditorController logPropertiesEditorController; - - @FXML - private Label userFieldLabel; - @FXML - private Label passwordFieldLabel; - @FXML - private TextField userField; - @FXML - private PasswordField passwordField; - @FXML - private Label levelLabel; - @FXML - private TextField dateField; - @FXML - private ComboBox levelSelector; - @FXML - private Label titleLabel; - @FXML - private TextField titleField; - @FXML - private TextArea textArea; - @FXML - private Button addLogbooks; - @FXML - private Button addTags; - @FXML - private ToggleButton logbooksDropdownButton; - @FXML - private ToggleButton tagsDropdownButton; - @FXML - private Label logbooksLabel; - @FXML - private TextField logbooksSelection; - @FXML - private TextField tagsSelection; - - private final ContextMenu logbookDropDown = new ContextMenu(); - private final ContextMenu tagDropDown = new ContextMenu(); - - private final ObservableList selectedLogbooks = FXCollections.observableArrayList(); - private final ObservableList selectedTags = FXCollections.observableArrayList(); - - private ObservableList availableLogbooksAsStringList; - private ObservableList availableTagsAsStringList; - private Collection availableLogbooks; - private Collection availableTags; - - private ListSelectionPopOver tagsPopOver; - private ListSelectionPopOver logbooksPopOver; - - private final ObservableList availableLevels = FXCollections.observableArrayList(); - private final SimpleStringProperty titleProperty = new SimpleStringProperty(); - private final SimpleStringProperty descriptionProperty = new SimpleStringProperty(); - private final SimpleStringProperty selectedLevelProperty = new SimpleStringProperty(); - private final SimpleStringProperty usernameProperty = new SimpleStringProperty(); - private final SimpleStringProperty passwordProperty = new SimpleStringProperty(); - - private final LogEntry logEntry; - - private final SimpleBooleanProperty updateCredentials = new SimpleBooleanProperty(); - private final ReadOnlyBooleanProperty updateCredentialsProperty; - private final SimpleBooleanProperty inputValid = new SimpleBooleanProperty(false); - - private final LogFactory logFactory; - - private final SimpleBooleanProperty submissionInProgress = - new SimpleBooleanProperty(false); - - private final Long logId; - - /** - * Indicates if user has started editing. Only title and body are used to define dirty state. - */ - private boolean isDirty = false; - - /** - * Used to determine if a log entry is dirty. For a new log entry, comparison is made to empty string, for - * the reply case the original title is copied to this field. - */ - private String originalTitle = ""; - - - public LogEntryUpdateController(LogEntry logEntry, LogEntryCompletionHandler logEntryCompletionHandler) { - this.logId = logEntry.getId(); - this.completionHandler = logEntryCompletionHandler; - this.logFactory = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory); - updateCredentialsProperty = updateCredentials; - this.logEntry = logEntry; - } - - @FXML - public void initialize() { - - // This could be configured in the fxml, but then these UI components would not be visible - // in Scene Builder. - completionMessageLabel.textProperty().set(""); - progressIndicator.visibleProperty().bind(submissionInProgress); - - // Remote log service not reachable, so show error pane. - if (!checkConnectivity()) { - errorPane.visibleProperty().set(true); - editorPane.disableProperty().set(true); - return; - } - - submitButton.disableProperty().bind(Bindings.createBooleanBinding(() -> - !inputValid.get() || submissionInProgress.get(), - inputValid, submissionInProgress)); - completionMessageLabel.visibleProperty() - .bind(Bindings.createBooleanBinding(() -> completionMessageLabel.textProperty().isNotEmpty().get() && !submissionInProgress.get(), - completionMessageLabel.textProperty(), submissionInProgress)); - - cancelButton.disableProperty().bind(submissionInProgress); - - userField.textProperty().bindBidirectional(usernameProperty); - userField.textProperty().addListener((changeListener, oldVal, newVal) -> - { - if (newVal.trim().isEmpty()) - userFieldLabel.setTextFill(Color.RED); - else - userFieldLabel.setTextFill(Color.BLACK); - }); - - passwordField.textProperty().bindBidirectional(passwordProperty); - passwordField.textProperty().addListener((changeListener, oldVal, newVal) -> - { - if (newVal.trim().isEmpty()) - passwordFieldLabel.setTextFill(Color.RED); - else - passwordFieldLabel.setTextFill(Color.BLACK); - }); - - updateCredentialsProperty.addListener((changeListener, oldVal, newVal) -> - { - // This call back should be running on a background thread. Perform contents on JavaFX application thread. - Platform.runLater(() -> - { - userField.setText(usernameProperty.get()); - passwordField.setText(passwordProperty.get()); - - // Put focus on first required field that is empty. - if (userField.getText().isEmpty()) { - userField.requestFocus(); - } else if (passwordField.getText().isEmpty()) { - passwordField.requestFocus(); - } else if (titleField.getText() == null || titleField.getText().isEmpty()) { - titleField.requestFocus(); - } else { - textArea.requestFocus(); - } - }); - }); - if (Preferences.save_credentials) { - fetchStoredUserCredentials(); - } - - levelLabel.setText(LogbookUIPreferences.level_field_name); - // Sites may wish to define a different meaning and name for the "level" field. - OlogProperties ologProperties = new OlogProperties(); - String[] levelList = ologProperties.getPreferenceValue("levels").split(","); - availableLevels.addAll(Arrays.asList(levelList)); - levelSelector.setItems(availableLevels); - selectedLevelProperty.set(logEntry.getLevel() != null ? logEntry.getLevel() : availableLevels.get(0)); - - levelSelector.getSelectionModel().select(selectedLevelProperty.get()); - - dateField.setText(TimestampFormats.DATE_FORMAT.format(Instant.now())); - - titleField.textProperty().bindBidirectional(titleProperty); - titleProperty.addListener((changeListener, oldVal, newVal) -> - { - if (newVal.trim().isEmpty()) { - titleLabel.setTextFill(Color.RED); - } else { - titleLabel.setTextFill(Color.BLACK); - } - if (!newVal.equals(originalTitle)) { - isDirty = true; - } - }); - titleProperty.set(logEntry.getTitle()); - - textArea.textProperty().bindBidirectional(descriptionProperty); - if (logEntry.getSource() != null) { - descriptionProperty.set(logEntry.getSource()); - } - else if (logEntry.getDescription() != null) { - descriptionProperty.set(logEntry.getDescription()); - } - else { - descriptionProperty.set(""); - } - descriptionProperty.addListener((observable, oldValue, newValue) -> isDirty = true); - - Image tagIcon = ImageCache.getImage(LogEntryUpdateController.class, "/icons/add_tag.png"); - Image logbookIcon = ImageCache.getImage(LogEntryUpdateController.class, "/icons/logbook-16.png"); - Image downIcon = ImageCache.getImage(LogEntryUpdateController.class, "/icons/down_triangle.png"); - - addLogbooks.setGraphic(new ImageView(logbookIcon)); - addTags.setGraphic(new ImageView(tagIcon)); - logbooksDropdownButton.setGraphic(new ImageView(downIcon)); - tagsDropdownButton.setGraphic(new ImageView(downIcon)); - - logbooksSelection.textProperty().addListener((changeListener, oldVal, newVal) -> - { - if (newVal.trim().isEmpty()) - logbooksLabel.setTextFill(Color.RED); - else - logbooksLabel.setTextFill(Color.BLACK); - }); - - logbooksSelection.textProperty().bind(Bindings.createStringBinding(() -> { - if (selectedLogbooks.isEmpty()) { - return ""; - } - StringBuilder stringBuilder = new StringBuilder(); - selectedLogbooks.forEach(l -> stringBuilder.append(l).append(", ")); - String text = stringBuilder.toString(); - return text.substring(0, text.length() - 2); - }, selectedLogbooks)); - - tagsSelection.textProperty().bind(Bindings.createStringBinding(() -> { - if (selectedTags.isEmpty()) { - return ""; - } - StringBuilder stringBuilder = new StringBuilder(); - selectedTags.forEach(l -> stringBuilder.append(l).append(", ")); - String text = stringBuilder.toString(); - return text.substring(0, text.length() - 2); - }, selectedTags)); - - logbooksDropdownButton.focusedProperty().addListener((changeListener, oldVal, newVal) -> - { - if (!newVal && !tagDropDown.isShowing() && !logbookDropDown.isShowing()) - logbooksDropdownButton.setSelected(false); - }); - - tagsDropdownButton.focusedProperty().addListener((changeListener, oldVal, newVal) -> - { - if (!newVal && !tagDropDown.isShowing() && !tagDropDown.isShowing()) - tagsDropdownButton.setSelected(false); - }); - - inputValid.bind(Bindings.createBooleanBinding(() -> titleProperty.get() != null && !titleProperty.get().isEmpty() && - usernameProperty.get() != null && !usernameProperty.get().isEmpty() && - passwordProperty.get() != null && !passwordProperty.get().isEmpty() && - !selectedLogbooks.isEmpty(), - titleProperty, usernameProperty, passwordProperty, selectedLogbooks)); - - tagsPopOver = ListSelectionPopOver.create( - (tags, popOver) -> { - setSelectedTags(tags, selectedTags); - if (popOver.isShowing()) { - popOver.hide(); - } - }, - (tags, popOver) -> popOver.hide() - ); - logbooksPopOver = ListSelectionPopOver.create( - (logbooks, popOver) -> { - setSelectedLogbooks(logbooks, selectedLogbooks); - if (popOver.isShowing()) { - popOver.hide(); - } - }, - (logbooks, popOver) -> popOver.hide() - ); - - selectedTags.addListener((ListChangeListener) change -> { - List newSelection = new ArrayList<>(change.getList()); - tagsPopOver.setAvailable(availableTagsAsStringList, newSelection); - tagsPopOver.setSelected(newSelection); - }); - - selectedLogbooks.addListener((ListChangeListener) change -> { - List newSelection = new ArrayList<>(change.getList()); - logbooksPopOver.setAvailable(availableLogbooksAsStringList, newSelection); - logbooksPopOver.setSelected(newSelection); - }); - - // Note: logbooks and tags are retrieved asynchronously from service - setupLogbooksAndTags(); - retrieveAttachments(); - } - - /** - * Handler for Cancel button. Note that any selections in the {@link SelectionService} are - * cleared to prevent next launch of {@link SendToLogBookApp} - * to pick them up. - */ - @FXML - public void cancel() { - // Need to clear selections. - SelectionService.getInstance().clearSelection(""); - ((LogEntryUpdateStage) cancelButton.getScene().getWindow()).handleCloseEditor(isDirty, editorPane); - } - - @FXML - public void showHelp() { - new HelpViewer(LogbookUIPreferences.markup_help).show(); - } - - /** - * Handler for HTML preview button - */ - @FXML - public void showHtmlPreview() { - new PreviewViewer(getDescription(), attachmentsViewController.getAttachments()).show(); - } - - - @FXML - public void submit() { - - submissionInProgress.set(true); - - JobManager.schedule("Submit Log Entry Update", monitor -> { - OlogLog ologLog = new OlogLog(); - ologLog.setId(logId); - ologLog.setTitle(getTitle()); - ologLog.setDescription(getDescription()); - ologLog.setLevel(selectedLevelProperty.get()); - ologLog.setLogbooks(getSelectedLogbooks()); - ologLog.setTags(getSelectedTags()); -// ologLog.setAttachments(attachmentsViewController.getAttachments()); - ologLog.setProperties(logPropertiesEditorController.getProperties()); - - LogClient logClient = - logFactory.getLogClient(new SimpleAuthenticationToken(usernameProperty.get(), passwordProperty.get())); - LogEntry result; - try { - result = logClient.update(ologLog); - // Not dirty any more... - isDirty = false; - if (result != null) { - if (completionHandler != null) { - completionHandler.handleResult(result); - } - // Set username and password in secure store if submission of log entry completes successfully - if (Preferences.save_credentials) { - // Get the SecureStore. Store username and password. - try { - SecureStore store = new SecureStore(); - ScopedAuthenticationToken scopedAuthenticationToken = - new ScopedAuthenticationToken(AuthenticationScope.LOGBOOK, usernameProperty.get(), passwordProperty.get()); - store.setScopedAuthentication(scopedAuthenticationToken); - } catch (Exception ex) { - logger.log(Level.WARNING, "Secure Store file not found.", ex); - } - } - // This will close the editor - Platform.runLater(this::cancel); - } - } catch (LogbookException e) { - logger.log(Level.WARNING, "Unable to submit log entry", e); - Platform.runLater(() -> { - if (e.getCause() != null && e.getCause().getMessage() != null) { - completionMessageLabel.textProperty().setValue(e.getCause().getMessage()); - } else if (e.getMessage() != null) { - completionMessageLabel.textProperty().setValue(e.getMessage()); - } else { - completionMessageLabel.textProperty().setValue(org.phoebus.logbook.Messages.SubmissionFailed); - } - }); - } - submissionInProgress.set(false); - }); - } - - @FXML - public void setLevel() { - selectedLevelProperty.set(levelSelector.getSelectionModel().getSelectedItem()); - } - - public String getTitle() { - return titleProperty.get(); - } - - public String getDescription() { - return descriptionProperty.get(); - } - - @FXML - public void addLogbooks() { - logbooksPopOver.show(addLogbooks); - } - - private void addSelectedLogbook(String logbookName) { - selectedLogbooks.add(logbookName); - updateDropDown(logbookDropDown, logbookName, true); - } - - private void removeSelectedLogbook(String logbookName) { - selectedLogbooks.remove(logbookName); - updateDropDown(logbookDropDown, logbookName, false); - } - - private void setSelectedLogbooks(List proposedLogbooks, List existingLogbooks) { - setSelected(proposedLogbooks, existingLogbooks, this::addSelectedLogbook, this::removeSelectedLogbook); - } - - private void setSelected(List proposed, List existing, Consumer addFunction, Consumer removeFunction) { - List addedTags = proposed.stream() - .filter(tag -> !existing.contains(tag)) - .collect(Collectors.toList()); - List removedTags = existing.stream() - .filter(tag -> !proposed.contains(tag)) - .collect(Collectors.toList()); - addedTags.forEach(addFunction); - removedTags.forEach(removeFunction); - } - - @FXML - public void selectLogbooks() { - if (logbooksDropdownButton.isSelected()) { - logbookDropDown.show(logbooksSelection, Side.BOTTOM, 0, 0); - } else { - logbookDropDown.hide(); - } - } - - @FXML - public void addTags() { - tagsPopOver.show(addTags); - } - - private void addSelectedTag(String tagName) { - selectedTags.add(tagName); - updateDropDown(tagDropDown, tagName, true); - } - - private void removeSelectedTag(String tagName) { - selectedTags.remove(tagName); - updateDropDown(tagDropDown, tagName, false); - } - - private void setSelectedTags(List proposedTags, List existingTags) { - setSelected(proposedTags, existingTags, this::addSelectedTag, this::removeSelectedTag); - } - - @FXML - public void selectTags() { - if (tagsDropdownButton.isSelected()) { - tagDropDown.show(tagsSelection, Side.BOTTOM, 0, 0); - } else { - tagDropDown.hide(); - } - } - - public List getSelectedLogbooks() { - return availableLogbooks.stream().filter(l -> selectedLogbooks.contains(l.getName())).collect(Collectors.toList()); - } - - public List getSelectedTags() { - return availableTags.stream().filter(t -> selectedTags.contains(t.getName())).collect(Collectors.toList()); - } - - /** - * Retrieves logbooks and tags from service and populates all the data structures that depend - * on the result. The call to the remote service is asynchronous. - */ - private void setupLogbooksAndTags() { - JobManager.schedule("Fetch Logbooks and Tags", monitor -> - { - LogClient logClient = - LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); - availableLogbooks = logClient.listLogbooks(); - availableLogbooksAsStringList = - FXCollections.observableArrayList(availableLogbooks.stream().map(Logbook::getName).collect(Collectors.toList())); - Collections.sort(availableLogbooksAsStringList); - - List preSelectedLogbooks = - logEntry.getLogbooks().stream().map(Logbook::getName).collect(Collectors.toList()); - List defaultLogbooks = Arrays.asList(LogbookUIPreferences.default_logbooks); - availableLogbooksAsStringList.forEach(logbook -> { - CheckBox checkBox = new CheckBox(logbook); - CustomMenuItem newLogbook = new CustomMenuItem(checkBox); - newLogbook.setHideOnClick(false); - checkBox.setOnAction(e -> { - CheckBox source = (CheckBox) e.getSource(); - String text = source.getText(); - if (source.isSelected()) { - selectedLogbooks.add(text); - } else { - selectedLogbooks.remove(text); - } - }); - if (!preSelectedLogbooks.isEmpty() && preSelectedLogbooks.contains(logbook)) { - checkBox.setSelected(preSelectedLogbooks.contains(logbook)); - selectedLogbooks.add(logbook); - } else if (defaultLogbooks.contains(logbook) && selectedLogbooks.isEmpty()) { - checkBox.setSelected(defaultLogbooks.contains(logbook)); - selectedLogbooks.add(logbook); - } - logbookDropDown.getItems().add(newLogbook); - }); - - availableTags = logClient.listTags(); - availableTagsAsStringList = - FXCollections.observableArrayList(availableTags.stream().map(Tag::getName).collect(Collectors.toList())); - Collections.sort(availableLogbooksAsStringList); - - List preSelectedTags = - logEntry.getTags().stream().map(Tag::getName).collect(Collectors.toList()); - availableTagsAsStringList.forEach(tag -> { - CheckBox checkBox = new CheckBox(tag); - CustomMenuItem newTag = new CustomMenuItem(checkBox); - newTag.setHideOnClick(false); - checkBox.setOnAction(e -> { - CheckBox source = (CheckBox) e.getSource(); - String text = source.getText(); - if (source.isSelected()) { - selectedTags.add(text); - } else { - selectedTags.remove(text); - } - }); - checkBox.setSelected(preSelectedTags.contains(tag)); - if (preSelectedTags.contains(tag)) { - selectedTags.add(tag); - } - tagDropDown.getItems().add(newTag); - }); - - tagsPopOver.setAvailable(availableTagsAsStringList, selectedTags); - tagsPopOver.setSelected(selectedTags); - logbooksPopOver.setAvailable(availableLogbooksAsStringList, selectedLogbooks); - logbooksPopOver.setSelected(selectedLogbooks); - - }); - } - - private void retrieveAttachments() { - JobManager.schedule("Fetch attachment data", monitor -> { - - LogClient logClient = - LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); - Collection attachments = logEntry.getAttachments().stream() - .filter((attachment) -> attachment.getName() != null && !attachment.getName().isEmpty()) - .map((attachment) -> { - OlogAttachment fileAttachment = new OlogAttachment(); - fileAttachment.setContentType(attachment.getContentType()); - fileAttachment.setThumbnail(false); - fileAttachment.setFileName(attachment.getName()); - try { - Path temp = Files.createTempFile("phoebus", attachment.getName()); - Files.copy(logClient.getAttachment(logEntry.getId(), attachment.getName()), temp, StandardCopyOption.REPLACE_EXISTING); - fileAttachment.setFile(temp.toFile()); - temp.toFile().deleteOnExit(); - } catch (LogbookException | IOException e) { - Logger.getLogger(SingleLogEntryDisplayController.class.getName()) - .log(Level.WARNING, "Failed to retrieve attachment " + fileAttachment.getFileName(), e); - } - return fileAttachment; - }).collect(Collectors.toList()); - // Update UI - Platform.runLater(() -> { - attachmentsViewController.setAttachments(attachments); - }); - }); - } - - public void fetchStoredUserCredentials() { - // Perform file IO on background thread. - JobManager.schedule("Access Secure Store", monitor -> - { - // Get the SecureStore. Retrieve username and password. - try { - SecureStore store = new SecureStore(); - ScopedAuthenticationToken scopedAuthenticationToken = store.getScopedAuthenticationToken(AuthenticationScope.LOGBOOK); - // Could be accessed from JavaFX Application Thread when updating, so synchronize. - synchronized (usernameProperty) { - usernameProperty.set(scopedAuthenticationToken == null ? "" : scopedAuthenticationToken.getUsername()); - } - synchronized (passwordProperty) { - passwordProperty.set(scopedAuthenticationToken == null ? "" : scopedAuthenticationToken.getPassword()); - } - // Let anyone listening know that their credentials are now out of date. - updateCredentials.set(true); - } catch (Exception ex) { - logger.log(Level.WARNING, "Secure Store file not found.", ex); - } - }); - } - - /** - * Updates the logbooks or tags context menu to reflect the state of selected logbooks and tags. - * - * @param contextMenu The context menu to update - * @param itemName The logbook or tag name identifying to a context menu item - * @param itemSelected Indicates whether to select or deselect. - */ - private void updateDropDown(ContextMenu contextMenu, String itemName, boolean itemSelected) { - for (MenuItem menuItem : contextMenu.getItems()) { - CustomMenuItem custom = (CustomMenuItem) menuItem; - CheckBox check = (CheckBox) custom.getContent(); - if (check.getText().equals(itemName)) { - check.setSelected(itemSelected); - break; - } - } - } - - public boolean isDirty() { - return isDirty; - } - - /** - * Checks connectivity to remote service by querying the info end-point. If connection fails, - * connectionError property is set to true, which should set opacity of the editor pane, and - * set visibility of error pane. - */ - private boolean checkConnectivity() { - LogClient logClient = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); - try { - logClient.serviceInfo(); - return true; - } catch (Exception e) { - Logger.getLogger(SendToLogBookApp.class.getName()).warning("Failed to query logbook service, it may be off-line."); - return false; - } - } -} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateStage.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateStage.java deleted file mode 100644 index 229da95c04..0000000000 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogEntryUpdateStage.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2019 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.logbook.olog.ui.write; - -import javafx.fxml.FXMLLoader; -import javafx.scene.Node; -import javafx.scene.Scene; -import javafx.scene.control.Alert; -import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.ButtonBar; -import javafx.scene.control.ButtonType; -import javafx.stage.Modality; -import javafx.stage.Stage; -import org.phoebus.framework.nls.NLS; -import org.phoebus.logbook.LogEntry; -import org.phoebus.logbook.olog.ui.AttachmentsViewController; -import org.phoebus.logbook.olog.ui.Messages; -import org.phoebus.ui.dialog.DialogHelper; - -import java.util.Collection; -import java.util.ResourceBundle; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class LogEntryUpdateStage extends Stage { - private LogEntryUpdateController logEntryUpdateController; - - /** - * A stand-alone window containing components needed to create a logbook entry. - * - * @param logEntry Pre-populated data for the log entry, e.g. date and (optionally) screen shot. - */ - public LogEntryUpdateStage(LogEntry logEntry) { - this(logEntry, null); - } - - /** - * A stand-alone window containing components needed to create a logbook entry. - * - * @param logEntry Pre-populated data for the log entry, e.g. date and (optionally) screen shot. - * @param completionHandler A completion handler called when service call completes. - */ - public LogEntryUpdateStage(LogEntry logEntry, LogEntryCompletionHandler completionHandler) { - - initModality(Modality.WINDOW_MODAL); - ResourceBundle resourceBundle = NLS.getMessages(Messages.class); - FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("LogEntryUpdate.fxml"), resourceBundle); - fxmlLoader.setControllerFactory(clazz -> { - try { - if (clazz.isAssignableFrom(LogEntryUpdateController.class)) { - logEntryUpdateController = (LogEntryUpdateController) clazz.getConstructor(LogEntry.class, LogEntryCompletionHandler.class) - .newInstance(logEntry, completionHandler); - return logEntryUpdateController; - } else if (clazz.isAssignableFrom(AttachmentsEditorController.class)) { - return clazz.getConstructor(LogEntry.class).newInstance(logEntry); - } else if (clazz.isAssignableFrom(AttachmentsViewController.class)) { - return clazz.getConstructor().newInstance(); - } else if (clazz.isAssignableFrom(LogPropertiesEditorController.class)) { - return clazz.getConstructor(Collection.class).newInstance(logEntry.getProperties()); - } - } catch (Exception e) { - Logger.getLogger(LogEntryUpdateStage.class.getName()).log(Level.SEVERE, "Failed to construct controller for log editor UI", e); - } - return null; - }); - - try { - fxmlLoader.load(); - } catch ( - Exception exception) { - Logger.getLogger(LogEntryUpdateStage.class.getName()).log(Level.WARNING, "Unable to load fxml for log entry editor UI", exception); - } - - Scene scene = new Scene(fxmlLoader.getRoot()); - setScene(scene); - scene.getWindow().setOnCloseRequest(we -> { - we.consume(); - handleCloseEditor(logEntryUpdateController.isDirty(), fxmlLoader.getRoot()); - }); - setTitle(Messages.EditLogEntry); - } - - /** - * Helper method to show a confirmation dialog if user closes/cancels log entry editor with "dirty" data. - * - * @param entryIsDirty Indicates if the log entry content (title or body, or both) have been changed. - * @param parent The {@link Node} used to determine the position of the dialog. - */ - public void handleCloseEditor(boolean entryIsDirty, Node parent) { - if (entryIsDirty) { - ButtonType discardChanges = new ButtonType(Messages.CloseRequestButtonDiscard, ButtonBar.ButtonData.OK_DONE); - ButtonType continueEditing = new ButtonType(Messages.CloseRequestButtonContinue, ButtonBar.ButtonData.CANCEL_CLOSE); - Alert alert = new Alert(AlertType.CONFIRMATION, - null, - discardChanges, - continueEditing); - alert.setHeaderText(Messages.CloseRequestHeader); - DialogHelper.positionDialog(alert, parent, -200, -300); - if (alert.showAndWait().get().getButtonData().equals(ButtonBar.ButtonData.OK_DONE)) { - close(); - } - } else { - close(); - } - } -} diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogPropertiesEditorController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogPropertiesEditorController.java index f59ba0e764..cb520e094e 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogPropertiesEditorController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/write/LogPropertiesEditorController.java @@ -173,6 +173,24 @@ public List getProperties() { return selectedProperties; } + /** + * @param properties {@link Collection} of {@link Property}s to set in the editor. + */ + public void setProperties(Collection properties){ + if(properties != null){ + selectedProperties.addAll(properties); + availableProperties.removeAll(properties); + } + } + + /** + * Moves all selected {@link Property}s back to list of available. + */ + public void clearSelectedProperties(){ + availableProperties.addAll(selectedProperties); + selectedProperties.clear(); + } + /** * Move the user selected available properties from the available list to the selected properties tree view */ diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties index d8e8e53511..21bd2841f8 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/messages.properties @@ -16,6 +16,7 @@ Attachments=Attachments AttachmentsDirectoryFailedCreate=Failed to create directory {0} for attachments AttachmentsFileNotDirectory=File {0} exists but is not a directory AttachmentsSearchProperty=Attachments: +AvailableTemplates=Available Templates Back=Back BrowseButton=Browse Cancel=Cancel @@ -87,6 +88,7 @@ Remove_Tooltip=Remove the selected items. RequestTooLarge=Total file size exceeds limit, selected file(s) not added. Reply=Reply ReplyToLogEntry=Reply To Log Entry +SaveAsTemplate=Save as template ScalingFactor=Scaling Factor SearchButtonText=Search: Search=Search available: @@ -97,6 +99,7 @@ SelectFolder=Select Folder SelectFile=Select Image File SelectLevelTooltip=Select the log entry level. SelectLogEntry=Select Log Entry +SelectTemplate=Select Template ServiceConnectionError=Unable to connect to the logbook service ShowHelp=Show Help ShowHideDetails=Show/Hide Details @@ -110,6 +113,7 @@ SubmitTooltip=Submit Log Entry Tags=Tags: TagsTitle=Select Tags TagsTooltip=Add tag to the log entry. +Templates=Templates: Text=Text: Time=Time: Title=Title: diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/write/LogEntryEditor.fxml b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/write/LogEntryEditor.fxml index b9a9803de1..92b89d57b4 100644 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/write/LogEntryEditor.fxml +++ b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/write/LogEntryEditor.fxml @@ -23,7 +23,7 @@ - + @@ -45,6 +45,13 @@ + + + + @@ -156,7 +163,7 @@ - + @@ -219,8 +226,7 @@ - diff --git a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/write/LogEntryUpdate.fxml b/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/write/LogEntryUpdate.fxml deleted file mode 100644 index f5d54a1ce2..0000000000 --- a/app/logbook/olog/ui/src/main/resources/org/phoebus/logbook/olog/ui/write/LogEntryUpdate.fxml +++ /dev/null @@ -1,232 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -