diff --git a/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEntry.java b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEntry.java new file mode 100644 index 00000000..cf2777bd --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEntry.java @@ -0,0 +1,19 @@ +package p.studio.controls.shell; + +import java.util.Objects; + +public record StudioActivityEntry( + String source, + String message, + StudioActivityEntrySeverity severity, + boolean sticky) { + + public StudioActivityEntry { + source = Objects.requireNonNull(source, "source").trim(); + message = Objects.requireNonNull(message, "message").trim(); + Objects.requireNonNull(severity, "severity"); + if (source.isBlank() || message.isBlank()) { + throw new IllegalArgumentException("source and message must not be blank"); + } + } +} diff --git a/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEntrySeverity.java b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEntrySeverity.java new file mode 100644 index 00000000..3bf40694 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEntrySeverity.java @@ -0,0 +1,8 @@ +package p.studio.controls.shell; + +public enum StudioActivityEntrySeverity { + INFO, + SUCCESS, + WARNING, + ERROR +} diff --git a/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEventMapper.java b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEventMapper.java new file mode 100644 index 00000000..5d33176f --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityEventMapper.java @@ -0,0 +1,30 @@ +package p.studio.controls.shell; + +import p.studio.events.*; + +import java.util.Optional; + +public final class StudioActivityEventMapper { + private StudioActivityEventMapper() { + } + + public static Optional map(StudioEvent event) { + return switch (event) { + case StudioProjectOpenedEvent opened -> + Optional.of(new StudioActivityEntry("Studio", "Project opened: " + opened.project().name(), StudioActivityEntrySeverity.SUCCESS, false)); + case StudioProjectLoadingStartedEvent started -> + Optional.of(new StudioActivityEntry("Studio", "Project loading started: " + started.project().name(), StudioActivityEntrySeverity.INFO, false)); + case StudioProjectLoadingCompletedEvent completed -> + Optional.of(new StudioActivityEntry("Studio", "Project ready: " + completed.project().name(), StudioActivityEntrySeverity.SUCCESS, false)); + case StudioProjectLoadingFailedEvent failed -> + Optional.of(new StudioActivityEntry("Studio", failed.message(), StudioActivityEntrySeverity.ERROR, true)); + case StudioAssetsWorkspaceRefreshStartedEvent ignored -> + Optional.of(new StudioActivityEntry("Assets", "Asset scan started", StudioActivityEntrySeverity.INFO, false)); + case StudioAssetsWorkspaceRefreshedEvent refreshed -> + Optional.of(new StudioActivityEntry("Assets", refreshed.assetCount() + " assets loaded", StudioActivityEntrySeverity.SUCCESS, false)); + case StudioAssetsWorkspaceRefreshFailedEvent failed -> + Optional.of(new StudioActivityEntry("Assets", failed.message(), StudioActivityEntrySeverity.ERROR, true)); + default -> Optional.empty(); + }; + } +} diff --git a/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityFeedControl.java b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityFeedControl.java new file mode 100644 index 00000000..a8d36299 --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/controls/shell/StudioActivityFeedControl.java @@ -0,0 +1,166 @@ +package p.studio.controls.shell; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.ProgressBar; +import javafx.scene.layout.VBox; +import p.studio.Container; +import p.studio.controls.lifecycle.StudioControlLifecycle; +import p.studio.controls.lifecycle.StudioControlLifecycleSupport; +import p.studio.events.*; +import p.studio.utilities.events.EventSubscription; +import p.studio.utilities.i18n.I18n; + +import java.util.ArrayList; +import java.util.List; + +public final class StudioActivityFeedControl extends VBox implements StudioControlLifecycle { + private final ObservableList entries = FXCollections.observableArrayList(); + private final List subscriptions = new ArrayList<>(); + private final Label progressLabel = new Label(); + private final ProgressBar progressBar = new ProgressBar(); + private final ListView listView = new ListView<>(entries); + + public StudioActivityFeedControl() { + StudioControlLifecycleSupport.install(this, this); + getStyleClass().add("studio-activity-feed"); + setSpacing(10); + setPadding(new Insets(12)); + + progressLabel.setText(Container.i18n().text(I18n.ACTIVITY_PROGRESS_IDLE)); + progressLabel.getStyleClass().add("studio-activity-progress-label"); + progressBar.getStyleClass().add("studio-activity-progress-bar"); + progressBar.setVisible(false); + progressBar.setManaged(false); + + listView.getStyleClass().add("studio-activity-list"); + listView.setCellFactory(ignored -> new ActivityCell()); + + getChildren().addAll(progressLabel, progressBar, listView); + } + + @Override + public void subscribe() { + final StudioEventBus eventBus = Container.events(); + subscriptions.add(eventBus.subscribe(StudioProjectOpenedEvent.class, this::onEvent)); + subscriptions.add(eventBus.subscribe(StudioProjectLoadingStartedEvent.class, this::onEvent)); + subscriptions.add(eventBus.subscribe(StudioProjectLoadingProgressEvent.class, this::onProjectLoadingProgress)); + subscriptions.add(eventBus.subscribe(StudioProjectLoadingCompletedEvent.class, this::onProjectLoadingCompleted)); + subscriptions.add(eventBus.subscribe(StudioProjectLoadingFailedEvent.class, this::onProjectLoadingFailed)); + subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshStartedEvent.class, this::onAssetsRefreshStarted)); + subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshedEvent.class, this::onAssetsRefreshFinished)); + subscriptions.add(eventBus.subscribe(StudioAssetsWorkspaceRefreshFailedEvent.class, this::onAssetsRefreshFailed)); + } + + @Override + public void unsubscribe() { + subscriptions.forEach(EventSubscription::unsubscribe); + subscriptions.clear(); + } + + private void onEvent(StudioEvent event) { + StudioActivityEventMapper.map(event).ifPresent(this::appendEntry); + } + + private void onProjectLoadingProgress(StudioProjectLoadingProgressEvent event) { + Platform.runLater(() -> { + progressLabel.setText(event.message()); + progressBar.setVisible(true); + progressBar.setManaged(true); + if (event.indeterminate()) { + progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + } else { + progressBar.setProgress(event.progress()); + } + }); + } + + private void onProjectLoadingCompleted(StudioProjectLoadingCompletedEvent event) { + onEvent(event); + clearProgress(); + } + + private void onProjectLoadingFailed(StudioProjectLoadingFailedEvent event) { + onEvent(event); + clearProgress(); + } + + private void onAssetsRefreshStarted(StudioAssetsWorkspaceRefreshStartedEvent event) { + onEvent(event); + Platform.runLater(() -> { + progressLabel.setText(Container.i18n().text(I18n.ACTIVITY_PROGRESS_ASSETS_REFRESH)); + progressBar.setVisible(true); + progressBar.setManaged(true); + progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); + }); + } + + private void onAssetsRefreshFinished(StudioAssetsWorkspaceRefreshedEvent event) { + onEvent(event); + clearProgress(); + } + + private void onAssetsRefreshFailed(StudioAssetsWorkspaceRefreshFailedEvent event) { + onEvent(event); + clearProgress(); + } + + private void clearProgress() { + Platform.runLater(() -> { + progressLabel.setText(Container.i18n().text(I18n.ACTIVITY_PROGRESS_IDLE)); + progressBar.setVisible(false); + progressBar.setManaged(false); + }); + } + + private void appendEntry(StudioActivityEntry entry) { + Platform.runLater(() -> { + if (!entries.isEmpty()) { + final StudioActivityEntry last = entries.getLast(); + if (last.source().equals(entry.source()) + && last.message().equals(entry.message()) + && last.severity() == entry.severity()) { + return; + } + } + entries.addFirst(entry); + while (entries.size() > 100) { + entries.removeLast(); + } + }); + } + + private static final class ActivityCell extends ListCell { + @Override + protected void updateItem(StudioActivityEntry item, boolean empty) { + super.updateItem(item, empty); + getStyleClass().removeAll( + "studio-activity-cell-info", + "studio-activity-cell-success", + "studio-activity-cell-warning", + "studio-activity-cell-error"); + + if (empty || item == null) { + setText(null); + setGraphic(null); + return; + } + + final Label source = new Label(item.source()); + source.getStyleClass().add("studio-activity-cell-source"); + final Label message = new Label(item.message()); + message.getStyleClass().add("studio-activity-cell-message"); + message.setWrapText(true); + final VBox content = new VBox(4, source, message); + content.setAlignment(Pos.CENTER_LEFT); + setGraphic(content); + getStyleClass().add("studio-activity-cell-" + item.severity().name().toLowerCase()); + } + } +} diff --git a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java index 61a5b56d..f318565f 100644 --- a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java +++ b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java @@ -117,6 +117,10 @@ public enum I18n { ASSETS_LABEL_PRELOAD("assets.label.preload"), ASSETS_VALUE_YES("assets.value.yes"), ASSETS_VALUE_NO("assets.value.no"), + ASSETS_PROGRESS_IDLE("assets.progress.idle"), + ASSETS_PROGRESS_REFRESHING("assets.progress.refreshing"), + ASSETS_PROGRESS_LOADING_DETAILS("assets.progress.loadingDetails"), + ASSETS_LOGS_TITLE("assets.logs.title"), ASSETS_INPUTS_EMPTY("assets.inputs.empty"), ASSETS_DIAGNOSTICS_EMPTY("assets.diagnostics.empty"), ASSETS_PREVIEW_EMPTY("assets.preview.empty"), @@ -124,6 +128,8 @@ public enum I18n { ASSETS_PREVIEW_IMAGE_ERROR("assets.preview.imageError"), ASSETS_PREVIEW_AUDIO_PLACEHOLDER("assets.preview.audioPlaceholder"), ASSETS_PREVIEW_GENERIC_PLACEHOLDER("assets.preview.genericPlaceholder"), + ACTIVITY_PROGRESS_IDLE("activity.progress.idle"), + ACTIVITY_PROGRESS_ASSETS_REFRESH("activity.progress.assetsRefresh"), ASSETS_SUMMARY_LOADING("assets.summary.loading"), ASSETS_SUMMARY_EMPTY("assets.summary.empty"), ASSETS_SUMMARY_READY("assets.summary.ready"), diff --git a/prometeu-studio/src/main/java/p/studio/window/MainView.java b/prometeu-studio/src/main/java/p/studio/window/MainView.java index 72cb4f5a..8af9bd65 100644 --- a/prometeu-studio/src/main/java/p/studio/window/MainView.java +++ b/prometeu-studio/src/main/java/p/studio/window/MainView.java @@ -43,7 +43,7 @@ public final class MainView extends BorderPane { setRight(new StudioRightUtilityPanelControl( runSurface, Container.i18n().bind(I18n.SHELL_ACTIVITY), - StudioRightUtilityPanelControl.createPlaceholderContent(Container.i18n().bind(I18n.SHELL_ACTIVITY)))); + new StudioActivityFeedControl())); // default workspaceRail.select(WorkspaceId.ASSETS); diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java index ace1d64e..9549a6e5 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/assets/AssetWorkspace.java @@ -36,9 +36,12 @@ public final class AssetWorkspace implements Workspace { private final FlowPane filterBar = new FlowPane(); private final Label navigatorStateLabel = new Label(); private final VBox navigatorContent = new VBox(8); + private final Label inlineProgressLabel = new Label(); + private final ProgressBar inlineProgressBar = new ProgressBar(); private final VBox detailsContent = new VBox(12); private final Label workspaceSummaryLabel = new Label(); + private final TextArea logsArea = new TextArea(); private final Map filterButtons = new EnumMap<>(AssetNavigatorFilter.class); private final EnumSet activeFilters = EnumSet.noneOf(AssetNavigatorFilter.class); @@ -61,6 +64,7 @@ public final class AssetWorkspace implements Workspace { root.getStyleClass().add("assets-workspace"); root.setCenter(buildLayout()); + root.setBottom(buildLogsPane()); renderState(); } @@ -88,7 +92,15 @@ public final class AssetWorkspace implements Workspace { return state; } - private SplitPane buildLayout() { + private VBox buildLayout() { + inlineProgressLabel.getStyleClass().add("assets-workspace-inline-progress-label"); + inlineProgressLabel.setText(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE)); + inlineProgressBar.getStyleClass().add("assets-workspace-inline-progress-bar"); + inlineProgressBar.setVisible(false); + inlineProgressBar.setManaged(false); + final VBox topProgress = new VBox(6, inlineProgressLabel, inlineProgressBar); + topProgress.getStyleClass().add("assets-workspace-inline-progress"); + final VBox navigatorPane = new VBox(8); navigatorPane.getStyleClass().add("assets-workspace-pane"); final Label navigatorTitle = new Label(); @@ -133,7 +145,24 @@ public final class AssetWorkspace implements Workspace { final SplitPane splitPane = new SplitPane(navigatorPane, detailsPane); splitPane.setDividerPositions(0.34); splitPane.getStyleClass().add("assets-workspace-split"); - return splitPane; + final VBox layout = new VBox(10, topProgress, splitPane); + VBox.setVgrow(splitPane, Priority.ALWAYS); + return layout; + } + + private TitledPane buildLogsPane() { + logsArea.setEditable(false); + logsArea.setWrapText(true); + logsArea.setPrefRowCount(8); + logsArea.getStyleClass().add("assets-workspace-logs"); + + final TitledPane pane = new TitledPane(); + pane.textProperty().bind(Container.i18n().bind(I18n.ASSETS_LOGS_TITLE)); + pane.setContent(logsArea); + pane.setCollapsible(true); + pane.setExpanded(true); + pane.getStyleClass().add("assets-workspace-logs-pane"); + return pane; } private void configureFilterBar() { @@ -169,6 +198,8 @@ public final class AssetWorkspace implements Workspace { selectedAssetDetails = null; detailsErrorMessage = null; selectedPreviewInput = null; + setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_REFRESHING), ProgressBar.INDETERMINATE_PROGRESS, true); + appendLog("Assets refresh started."); renderState(); workspaceBus.publish(new StudioAssetsWorkspaceRefreshStartedEvent(projectReference)); @@ -179,12 +210,16 @@ public final class AssetWorkspace implements Workspace { state = AssetWorkspaceState.error(state, rootCauseMessage(throwable)); detailsStatus = AssetWorkspaceDetailsStatus.ERROR; detailsErrorMessage = state.errorMessage(); + setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false); + appendLog("Assets refresh failed: " + state.errorMessage()); renderState(); workspaceBus.publish(new StudioAssetsWorkspaceRefreshFailedEvent(projectReference, state.errorMessage())); return; } state = AssetWorkspaceState.ready(snapshot.assets(), state.selectedKey()); + setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false); + appendLog("Assets refresh completed: " + state.assets().size() + " assets."); renderState(); workspaceBus.publish(new StudioAssetsWorkspaceRefreshedEvent(projectReference, state.assets().size())); state.selectedAsset().ifPresent(asset -> { @@ -199,6 +234,8 @@ public final class AssetWorkspace implements Workspace { selectedAssetDetails = null; detailsErrorMessage = null; selectedPreviewInput = null; + setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_LOADING_DETAILS), ProgressBar.INDETERMINATE_PROGRESS, true); + appendLog("Loading details for " + selectionKey.stableKey() + "."); renderState(); CompletableFuture @@ -211,6 +248,8 @@ public final class AssetWorkspace implements Workspace { detailsStatus = AssetWorkspaceDetailsStatus.ERROR; detailsErrorMessage = rootCauseMessage(throwable); selectedAssetDetails = null; + setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false); + appendLog("Asset details failed: " + detailsErrorMessage); renderState(); return; } @@ -218,6 +257,8 @@ public final class AssetWorkspace implements Workspace { selectedAssetDetails = details; detailsStatus = AssetWorkspaceDetailsStatus.READY; selectedPreviewInput = firstPreviewInput(details); + setInlineProgress(Container.i18n().text(I18n.ASSETS_PROGRESS_IDLE), 0, false); + appendLog("Asset details ready for " + details.summary().assetName() + "."); renderState(); })); } @@ -617,6 +658,7 @@ public final class AssetWorkspace implements Workspace { private void selectAsset(AssetWorkspaceSelectionKey selectionKey) { state = state.withSelection(selectionKey); + appendLog("Selected asset " + selectionKey.stableKey() + "."); renderState(); workspaceBus.publish(new StudioAssetsWorkspaceSelectionChangedEvent(projectReference, selectionKey)); loadSelectedAssetDetails(selectionKey); @@ -698,6 +740,17 @@ public final class AssetWorkspace implements Workspace { return projectReference.rootPath().resolve("assets").toAbsolutePath().normalize(); } + private void setInlineProgress(String message, double progress, boolean visible) { + inlineProgressLabel.setText(message); + inlineProgressBar.setVisible(visible); + inlineProgressBar.setManaged(visible); + inlineProgressBar.setProgress(progress); + } + + private void appendLog(String message) { + logsArea.appendText(message + System.lineSeparator()); + } + private String rootCauseMessage(Throwable throwable) { Throwable current = throwable; while (current.getCause() != null) { diff --git a/prometeu-studio/src/main/resources/i18n/messages.properties b/prometeu-studio/src/main/resources/i18n/messages.properties index c84b923d..daad6666 100644 --- a/prometeu-studio/src/main/resources/i18n/messages.properties +++ b/prometeu-studio/src/main/resources/i18n/messages.properties @@ -107,6 +107,10 @@ assets.label.codec=Codec assets.label.preload=Preload assets.value.yes=Yes assets.value.no=No +assets.progress.idle=Assets workspace idle. +assets.progress.refreshing=Refreshing assets... +assets.progress.loadingDetails=Loading selected asset details... +assets.logs.title=Logs assets.inputs.empty=No previewable inputs are currently declared for this asset. assets.diagnostics.empty=No diagnostics are currently attached to this asset. assets.preview.empty=Select an input to preview it here. @@ -114,6 +118,8 @@ assets.preview.textError=Unable to read this text-like input for preview. assets.preview.imageError=Unable to decode this image for preview. assets.preview.audioPlaceholder=Audio preview placeholder: {0} assets.preview.genericPlaceholder=Preview placeholder for {0} +activity.progress.idle=No background work in progress. +activity.progress.assetsRefresh=Refreshing asset workspace... assets.summary.loading=Hydrating asset workspace state... assets.summary.empty=No assets are currently available. assets.summary.ready=Navigator ready with {0} assets. diff --git a/prometeu-studio/src/main/resources/themes/default-prometeu.css b/prometeu-studio/src/main/resources/themes/default-prometeu.css index 13bfe0de..0959e116 100644 --- a/prometeu-studio/src/main/resources/themes/default-prometeu.css +++ b/prometeu-studio/src/main/resources/themes/default-prometeu.css @@ -113,6 +113,56 @@ -fx-padding: 16; } +.studio-activity-feed { + -fx-background-color: transparent; +} + +.studio-activity-progress-label { + -fx-text-fill: #d4e0ec; + -fx-font-size: 12px; +} + +.studio-activity-progress-bar { + -fx-accent: #5cb6ff; +} + +.studio-activity-list { + -fx-background-color: transparent; + -fx-control-inner-background: #12161b; +} + +.studio-activity-list .list-cell { + -fx-background-color: transparent; + -fx-padding: 8; +} + +.studio-activity-cell-source { + -fx-text-fill: #8db7e1; + -fx-font-size: 11px; + -fx-font-weight: bold; +} + +.studio-activity-cell-message { + -fx-text-fill: #f1f6fb; + -fx-font-size: 12px; +} + +.studio-activity-cell-info { + -fx-background-color: #16222f; +} + +.studio-activity-cell-success { + -fx-background-color: #143020; +} + +.studio-activity-cell-warning { + -fx-background-color: #3f3115; +} + +.studio-activity-cell-error { + -fx-background-color: #40191d; +} + .assets-workspace { -fx-background-color: #17191d; } @@ -127,6 +177,19 @@ -fx-background-color: #1b1f25; } +.assets-workspace-inline-progress { + -fx-padding: 16 18 0 18; +} + +.assets-workspace-inline-progress-label { + -fx-text-fill: #d4dce5; + -fx-font-size: 12px; +} + +.assets-workspace-inline-progress-bar { + -fx-accent: #5cb6ff; +} + .assets-workspace-pane-title { -fx-text-fill: #f3f7fb; -fx-font-size: 15px; @@ -417,6 +480,16 @@ -fx-text-fill: #ffd6d9; } +.assets-workspace-logs-pane { + -fx-collapsible: true; +} + +.assets-workspace-logs { + -fx-control-inner-background: #0f1318; + -fx-text-fill: #dbe4ed; + -fx-highlight-fill: #2c5e91; +} + .studio-project-launcher { -fx-background-color: linear-gradient(to bottom, #20242c, #14181d); } diff --git a/prometeu-studio/src/test/java/p/studio/controls/shell/StudioActivityEventMapperTest.java b/prometeu-studio/src/test/java/p/studio/controls/shell/StudioActivityEventMapperTest.java new file mode 100644 index 00000000..c4a70a1b --- /dev/null +++ b/prometeu-studio/src/test/java/p/studio/controls/shell/StudioActivityEventMapperTest.java @@ -0,0 +1,51 @@ +package p.studio.controls.shell; + +import org.junit.jupiter.api.Test; +import p.studio.events.StudioAssetsWorkspaceRefreshFailedEvent; +import p.studio.events.StudioAssetsWorkspaceRefreshedEvent; +import p.studio.events.StudioProjectOpenedEvent; +import p.studio.projects.ProjectReference; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class StudioActivityEventMapperTest { + @Test + void mapsProjectOpenedToSuccessEntry() { + final StudioActivityEntry entry = StudioActivityEventMapper + .map(new StudioProjectOpenedEvent(project())) + .orElseThrow(); + + assertEquals("Studio", entry.source()); + assertEquals(StudioActivityEntrySeverity.SUCCESS, entry.severity()); + assertTrue(entry.message().contains("Project opened")); + } + + @Test + void mapsAssetsRefreshedToSuccessEntry() { + final StudioActivityEntry entry = StudioActivityEventMapper + .map(new StudioAssetsWorkspaceRefreshedEvent(project(), 7)) + .orElseThrow(); + + assertEquals("Assets", entry.source()); + assertEquals(StudioActivityEntrySeverity.SUCCESS, entry.severity()); + assertEquals("7 assets loaded", entry.message()); + } + + @Test + void mapsRefreshFailureToStickyErrorEntry() { + final StudioActivityEntry entry = StudioActivityEventMapper + .map(new StudioAssetsWorkspaceRefreshFailedEvent(project(), "Refresh failed")) + .orElseThrow(); + + assertEquals(StudioActivityEntrySeverity.ERROR, entry.severity()); + assertTrue(entry.sticky()); + assertEquals("Refresh failed", entry.message()); + } + + private ProjectReference project() { + return new ProjectReference("Main", "1.0.0", "pbs", 1, Path.of("/tmp/main")); + } +}