From 67852ad6dbdc2268c0fbcb06c25879eec3c2a62a Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 27 Mar 2026 10:19:02 +0100 Subject: [PATCH 1/5] Include attachments when updating a logbook entry --- .../logbook/olog/ui/write/LogEntryEditorController.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 f5b0008876..abbac408e3 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 @@ -734,13 +734,11 @@ public void submit() { ologLog.setLevel(selectedLevelProperty.get()); ologLog.setLogbooks(getSelectedLogbooks()); ologLog.setTags(getSelectedTags()); - if (editMode.equals(EditMode.NEW_LOG_ENTRY)) { - ologLog.setAttachments(attachmentsEditorController.getAttachments()); - } - else{ + ologLog.setAttachments(attachmentsEditorController.getAttachments()); + ologLog.setProperties(logPropertiesEditorController.getProperties()); + if (editMode.equals(EditMode.UPDATE_LOG_ENTRY)) { ologLog.setId(logEntry.getId()); } - ologLog.setProperties(logPropertiesEditorController.getProperties()); LogClient logClient = logFactory.getLogClient(new SimpleAuthenticationToken(usernameProperty.get(), passwordProperty.get())); From 2a88788bbca39a720b4494b3f71cbbbb5aa244af Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 27 Mar 2026 14:19:52 +0100 Subject: [PATCH 2/5] Add UI components to manage attachments when editing log entry --- .../phoebus/olog/es/api/OlogHttpClient.java | 19 +++++++++++ .../olog/ui/AttachmentsViewController.java | 34 ++++++++++++++++++- .../ui/write/LogEntryEditorController.java | 1 - 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java index 48afecb0a8..875d4403ab 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java @@ -502,6 +502,25 @@ public InputStream getAttachment(Long logId, String attachmentName) { } } + @Override + public InputStream getAttachment(Long logId, Attachment attachment){ + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/attachment/" + attachment.getId())) + .GET() + .build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + if (response.statusCode() >= 300) { + LOGGER.log(Level.WARNING, "failed to obtain attachment: " + new String(response.body().readAllBytes())); + return null; + } + return response.body(); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "failed to obtain attachment", e); + return null; + } + } + /** * @param id Unique log entry id * @return A {@link SearchResult} containing a list of {@link LogEntry} objects representing the diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java index 8290b0dd53..9429651bab 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java @@ -47,7 +47,12 @@ import org.phoebus.framework.spi.AppResourceDescriptor; import org.phoebus.framework.workbench.ApplicationService; import org.phoebus.logbook.Attachment; +import org.phoebus.logbook.LogClient; import org.phoebus.logbook.LogEntry; +import org.phoebus.logbook.LogService; +import org.phoebus.logbook.LogbookException; +import org.phoebus.logbook.LogbookPreferences; +import org.phoebus.olog.es.api.OlogHttpClient; import org.phoebus.ui.application.ApplicationLauncherService; import org.phoebus.ui.application.PhoebusApplication; import org.phoebus.ui.dialog.ExceptionDetailsErrorDialog; @@ -57,6 +62,7 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.nio.file.Files; import java.nio.file.StandardCopyOption; @@ -231,6 +237,12 @@ public void invalidateAttachmentList(LogEntry logEntry) { attachments.setAll(Collections.emptyList()); } + /** + * Sets the list of {@link Attachment}s when UI is created. This list is non-empty for instance if + * log entry is created from OPI (in which case the attschment is automatically added to a log entry), + * or when editing an existing entry containing attachments. + * @param attachmentsList List of {@link Attachment}s + */ public void setAttachments(Collection attachmentsList) { Platform.runLater(() -> { this.attachments.setAll(attachmentsList); @@ -278,7 +290,7 @@ private void showPreview(Attachment attachment) { * @param attachment The image {@link Attachment} selected by user. */ private void showImagePreview(Attachment attachment) { - if (attachment.getFile() != null && attachment.getFile().exists()) { + if (attachment.getFile() != null && attachment.getFile().exists()) { // Attachment exists on disk // Load image data off UI thread... JobManager.schedule("Show image attachment", monitor -> { try { @@ -298,6 +310,26 @@ private void showImagePreview(Attachment attachment) { } }); } + else{ // Attachment must be retrieved from service + LogClient logClient = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); + try { + InputStream inputStream = logClient.getAttachment(-1L, attachment); + if(inputStream != null){ + BufferedImage bufferedImage = ImageIO.read(inputStream); + if (bufferedImage == null) { + return; + } + Platform.runLater(() -> { + Image image = SwingFXUtils.toFXImage(bufferedImage, null); + imagePreview.visibleProperty().setValue(true); + imagePreview.setImage(image); + }); + } + } catch (Exception ex) { + Logger.getLogger(AttachmentsViewController.class.getName()) + .log(Level.SEVERE, "Unable to get attachment " + attachment.getName() + " from service", ex); + } + } } /** 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 abbac408e3..15cee165d5 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 @@ -309,7 +309,6 @@ public void initialize() { 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. From b8a8ec0d83b6c8905b9d9559a280750aac2ff75e Mon Sep 17 00:00:00 2001 From: georgweiss Date: Fri, 27 Mar 2026 14:45:46 +0100 Subject: [PATCH 3/5] Code simplification --- .../olog/ui/AttachmentsViewController.java | 55 +++++++------------ 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java index 9429651bab..0215e7e3f6 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/AttachmentsViewController.java @@ -290,46 +290,33 @@ private void showPreview(Attachment attachment) { * @param attachment The image {@link Attachment} selected by user. */ private void showImagePreview(Attachment attachment) { - if (attachment.getFile() != null && attachment.getFile().exists()) { // Attachment exists on disk - // Load image data off UI thread... - JobManager.schedule("Show image attachment", monitor -> { - try { - BufferedImage bufferedImage = ImageIO.read(attachment.getFile()); - // BufferedImage may be null due to lazy loading strategy. - if (bufferedImage == null) { - return; - } - Platform.runLater(() -> { - Image image = SwingFXUtils.toFXImage(bufferedImage, null); - imagePreview.visibleProperty().setValue(true); - imagePreview.setImage(image); - }); - } catch (IOException ex) { - Logger.getLogger(AttachmentsViewController.class.getName()) - .log(Level.SEVERE, "Unable to load image file " + attachment.getFile().getAbsolutePath(), ex); - } - }); - } - else{ // Attachment must be retrieved from service - LogClient logClient = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); + JobManager.schedule("Show image attachment", monitor -> { try { - InputStream inputStream = logClient.getAttachment(-1L, attachment); - if(inputStream != null){ - BufferedImage bufferedImage = ImageIO.read(inputStream); - if (bufferedImage == null) { - return; + BufferedImage bufferedImage = null; + if (attachment.getFile() != null && attachment.getFile().exists()) { // Attachment exists on disk + bufferedImage = ImageIO.read(attachment.getFile()); + } + else { // Attachment must be retrieved from service + LogClient logClient = LogService.getInstance().getLogFactories().get(LogbookPreferences.logbook_factory).getLogClient(); + InputStream inputStream = logClient.getAttachment(-1L, attachment); + if (inputStream != null) { + bufferedImage = ImageIO.read(inputStream); } - Platform.runLater(() -> { - Image image = SwingFXUtils.toFXImage(bufferedImage, null); - imagePreview.visibleProperty().setValue(true); - imagePreview.setImage(image); - }); } + if (bufferedImage == null) { + return; + } + BufferedImage _bufferedImage = bufferedImage; + Platform.runLater(() -> { + Image image = SwingFXUtils.toFXImage(_bufferedImage, null); + imagePreview.visibleProperty().setValue(true); + imagePreview.setImage(image); + }); } catch (Exception ex) { Logger.getLogger(AttachmentsViewController.class.getName()) - .log(Level.SEVERE, "Unable to get attachment " + attachment.getName() + " from service", ex); + .log(Level.SEVERE, "Unable to load image file " + attachment.getName(), ex); } - } + }); } /** From 96dc07987789393316cec580e9022945f9437623 Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 30 Mar 2026 13:38:58 +0200 Subject: [PATCH 4/5] Support add/remove attachments when editing olog entry --- .../phoebus/olog/es/api/OlogHttpClient.java | 51 ++++++++---------- .../olog/ui/doc/images/ContextMenuEdit.png | Bin 0 -> 9644 bytes .../doc/images/ContextMenuLogEntryTable.png | Bin 7537 -> 9317 bytes app/logbook/olog/ui/doc/index.rst | 12 +++++ 4 files changed, 35 insertions(+), 28 deletions(-) create mode 100644 app/logbook/olog/ui/doc/images/ContextMenuEdit.png diff --git a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java index 875d4403ab..93abd96240 100644 --- a/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java +++ b/app/logbook/olog/client-es/src/main/java/org/phoebus/olog/es/api/OlogHttpClient.java @@ -32,6 +32,7 @@ import org.phoebus.util.http.HttpRequestMultipartBody; import org.phoebus.util.http.QueryParamsHelper; +import javax.ws.rs.HttpMethod; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.MultivaluedMap; import java.io.InputStream; @@ -158,7 +159,7 @@ private OlogHttpClient(String userName, String password) { @Override public LogEntry set(LogEntry log) throws LogbookException { - return save(log, null); + return save(log, null, HttpMethod.PUT); } /** @@ -171,7 +172,7 @@ public LogEntry set(LogEntry log) throws LogbookException { * @throws LogbookException E.g. due to invalid log entry data, or if attachment content type * cannot be determined. */ - private LogEntry save(LogEntry log, LogEntry inReplyTo) throws LogbookException { + private LogEntry save(LogEntry log, LogEntry inReplyTo, String method) throws LogbookException { try { javax.ws.rs.core.MultivaluedMap queryParams = new javax.ws.rs.core.MultivaluedHashMap<>(); queryParams.putSingle("markup", "commonmark"); @@ -183,24 +184,29 @@ private LogEntry save(LogEntry log, LogEntry inReplyTo) throws LogbookException httpRequestMultipartBody.addTextPart("logEntry", OlogObjectMappers.logEntrySerializer.writeValueAsString(log), "application/json"); for (Attachment attachment : log.getAttachments()) { - httpRequestMultipartBody.addFilePart(attachment.getFile(), attachment.getUniqueFilename()); + if(attachment.getFile() != null){ + httpRequestMultipartBody.addFilePart(attachment.getFile(), attachment.getUniqueFilename()); + } } + HttpRequest.BodyPublisher bodyPublisher = HttpRequest.BodyPublishers.ofByteArray(httpRequestMultipartBody.getBytes()); + HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logs/multipart?" + QueryParamsHelper.mapToQueryParams(queryParams))) .header("Content-Type", httpRequestMultipartBody.getContentType()) .header(OLOG_CLIENT_INFO_HEADER, CLIENT_INFO) .header("Authorization", basicAuthenticationHeader) - .PUT(HttpRequest.BodyPublishers.ofByteArray(httpRequestMultipartBody.getBytes())) + .method(method, bodyPublisher) + //.PUT(HttpRequest.BodyPublishers.ofByteArray(httpRequestMultipartBody.getBytes())) .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if(response.statusCode() == 401){ - LOGGER.log(Level.SEVERE, "Failed to create log entry: user not authenticated"); + LOGGER.log(Level.SEVERE, "Failed to create or update log entry: user not authenticated"); throw new LogbookException(Messages.SubmissionFailedInvalidCredentials); } else if (response.statusCode() >= 300) { - LOGGER.log(Level.SEVERE, "Failed to create log entry: " + response.body()); + LOGGER.log(Level.SEVERE, "Failed to create or update log entry: " + response.body()); throw new LogbookException(response.body()); } else { return OlogObjectMappers.logEntryDeserializer.readValue(response.body(), OlogLog.class); @@ -213,7 +219,7 @@ else if (response.statusCode() >= 300) { @Override public LogEntry reply(LogEntry log, LogEntry inReplyTo) throws LogbookException { - return save(log, inReplyTo); + return save(log, inReplyTo, HttpMethod.PUT); } /** @@ -305,32 +311,21 @@ public String getServiceUrl() { } /** - * Updates an existing {@link LogEntry}. Note that unlike the {@link #save(LogEntry, LogEntry)} API, - * this does not support attachments, i.e. it does not set up a multipart request to the service. + * Updates an existing {@link LogEntry}. + *

NOTE:

+ * The list of attachments in the {@link LogEntry} may contain additional attachments added by the user. Moreover, + * attachments listed in the {@link LogEntry} subject to change, but not listed in the {@link LogEntry} submitted + * to the service, will be removed. This makes it possible for a user to update an entry such that a + * potentially erroneous attachment is replaced by a correct one. * * @param logEntry - the updated log entry * @return The updated {@link LogEntry} */ @Override - public LogEntry update(LogEntry logEntry) { - - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(Preferences.olog_url + OLOG_PREFIX + "/logs/" + logEntry.getId() + "?markup=commonmark")) - .header("Content-Type", CONTENT_TYPE_JSON) - .header("Authorization", basicAuthenticationHeader) - .POST(HttpRequest.BodyPublishers.ofString(OlogObjectMappers.logEntrySerializer.writeValueAsString(logEntry))) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - LogEntry updated = OlogObjectMappers.logEntryDeserializer.readValue(response.body(), OlogLog.class); - changeHandlers.forEach(h -> h.logEntryChanged(updated)); - return updated; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Unable to update log entry id=" + logEntry.getId(), e); - return null; - } + public LogEntry update(LogEntry logEntry) throws LogbookException{ + LogEntry updated = save(logEntry, null, HttpMethod.POST); + changeHandlers.forEach(h -> h.logEntryChanged(updated)); + return updated; } @Override diff --git a/app/logbook/olog/ui/doc/images/ContextMenuEdit.png b/app/logbook/olog/ui/doc/images/ContextMenuEdit.png new file mode 100644 index 0000000000000000000000000000000000000000..a30e987df47fc699fdb3e729f3684bf594b79812 GIT binary patch literal 9644 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 P=|L(An)21MRuTULE#(=R delta 7107 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 diff --git a/app/logbook/olog/ui/doc/index.rst b/app/logbook/olog/ui/doc/index.rst index e6d2cb8c84..106f945c8a 100644 --- a/app/logbook/olog/ui/doc/index.rst +++ b/app/logbook/olog/ui/doc/index.rst @@ -266,6 +266,18 @@ Please consider the following limitations of the log entry group feature: .. _preferences: +Updating an existing log entry +------------------------------ + +Existing log entries may be modified with respect to all parts of the log entry, except author and create/modify date. +A right click on an item in the list view will bring up the context menu from which the user can select Edit, see below. + +**NOTE**: when editing a log entry, the attachments view will show the attachments added when the entry was created +or modified. In this view users may add further attachments, but also remove the ones added previously. Removed attachments +will then be accessible only when inspecting the history of the entry. + +.. image:: images/ContextMenuEdit.png + Preferences ----------- From 04e023c9e1a90e907c3859b725fffcf57bbf123d Mon Sep 17 00:00:00 2001 From: georgweiss Date: Mon, 30 Mar 2026 14:14:29 +0200 Subject: [PATCH 5/5] Fix for: embedded images not shown in preview when editing log entry --- .../olog/ui/OlogAttributeProvider.java | 34 +++++++++++-------- .../ui/write/AttachmentsEditorController.java | 3 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/OlogAttributeProvider.java b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/OlogAttributeProvider.java index c0e0b7e1c8..4121899338 100644 --- a/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/OlogAttributeProvider.java +++ b/app/logbook/olog/ui/src/main/java/org/phoebus/logbook/olog/ui/OlogAttributeProvider.java @@ -18,17 +18,15 @@ package org.phoebus.logbook.olog.ui; +import org.apache.commons.io.FilenameUtils; import org.commonmark.ext.gfm.tables.TableBlock; import org.commonmark.renderer.html.AttributeProvider; - -import java.util.Map; -import java.util.logging.Level; -import java.util.logging.Logger; import org.phoebus.logbook.Attachment; -import java.util.List; -import org.apache.commons.io.FilenameUtils; import org.phoebus.olog.es.api.OlogHttpClient; +import java.util.List; +import java.util.Map; + /** * An {@link AttributeProvider} used to style elements of a log entry. Other types of * attribute processing may be added. @@ -39,18 +37,19 @@ public class OlogAttributeProvider implements AttributeProvider { private boolean preview = false; private List attachments; - public OlogAttributeProvider(String serviceUrl){ + public OlogAttributeProvider(String serviceUrl) { this.serviceUrl = serviceUrl; } /** * This is constructor for HTML preview feature - * @param serviceUrl Olog service url. - * @param preview A boolean flag set true for HTML preview feature parsing. + * + * @param serviceUrl Olog service url. + * @param preview A boolean flag set true for HTML preview feature parsing. * @param attachments A list of current attachments which can be parsed to - * find attachment file path from attachment id. + * find attachment file path from attachment id. */ - public OlogAttributeProvider(String serviceUrl, boolean preview, List attachments){ + public OlogAttributeProvider(String serviceUrl, boolean preview, List attachments) { this.serviceUrl = serviceUrl; this.preview = preview; this.attachments = attachments; @@ -80,12 +79,17 @@ public void setAttributes(org.commonmark.node.Node node, String s, Map { if (a.getFile() != null) { attachedFilesSize += getFileSize(a.getFile()); + filesToDeleteAfterSubmit.add(a.getFile()); } }); } attachmentsViewController.setAttachments(attachments); - filesToDeleteAfterSubmit.addAll(attachments.stream().map(Attachment::getFile).toList()); - removeButton.setGraphic(ImageCache.getImageView(ImageCache.class, "/icons/delete.png")); removeButton.disableProperty().bind(Bindings.isEmpty(attachmentsViewController.getSelectedAttachments()));