From 41023f3f08fc19f11a8a726e85aa7c6002b9d607 Mon Sep 17 00:00:00 2001 From: David Stephan Date: Fri, 6 Mar 2026 16:32:20 +0100 Subject: [PATCH 1/9] SED-4417 Grid layout for Executions Report --- .../step/core/reporting/ReportLayout.java | 34 +++++ .../core/reporting/ReportLayoutAccessor.java | 30 +++++ .../core/reporting/ReportLayoutPlugin.java | 35 +++++ .../core/reporting/ReportLayoutServices.java | 126 ++++++++++++++++++ .../step/core/entities/EntityConstants.java | 1 + 5 files changed, 226 insertions(+) create mode 100644 step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java create mode 100644 step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java create mode 100644 step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java create mode 100644 step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java new file mode 100644 index 000000000..69fdb8bfe --- /dev/null +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java @@ -0,0 +1,34 @@ +package step.core.reporting; + +import java.util.Map; + +import jakarta.validation.constraints.NotNull; +import step.core.accessors.AbstractTrackedObject; + +public class ReportLayout extends AbstractTrackedObject { + + public static final String FIELD_IS_SHARED = "shared"; + + @NotNull + Map layout; + boolean shared = false; + + public ReportLayout() { + } + + public Map getLayout() { + return layout; + } + + public void setLayout(Map layout) { + this.layout = layout; + } + + public boolean isShared() { + return shared; + } + + public void setShared(boolean shared) { + this.shared = shared; + } +} diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java new file mode 100644 index 000000000..df04c3239 --- /dev/null +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java @@ -0,0 +1,30 @@ +package step.core.reporting; + +import step.core.accessors.AbstractAccessor; +import step.core.accessors.AbstractOrganizableObject; +import step.core.collections.Collection; +import step.core.collections.Filters; +import step.core.collections.SearchOrder; +import step.core.collections.filters.Or; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static step.core.collections.Order.ASC; +import static step.core.reporting.ReportLayout.FIELD_IS_SHARED; + +public class ReportLayoutAccessor extends AbstractAccessor { + + public ReportLayoutAccessor(Collection collectionDriver) { + super(collectionDriver); + } + + public List getAccessibleReportLayoutsDefinitions(String username) { + Or ownerOrShared = Filters.or(List.of(Filters.equals(FIELD_IS_SHARED, true), Filters.equals("creationUser", username))); + return this.getCollectionDriver() + .find(ownerOrShared, new SearchOrder(ATTRIBUTES_FIELD_NAME + "." + AbstractOrganizableObject.NAME, ASC.numeric), null, null, 0) + .peek(reportLayout -> reportLayout.setLayout(Map.of())) + .collect(Collectors.toList()); + } +} diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java new file mode 100644 index 000000000..035a68244 --- /dev/null +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java @@ -0,0 +1,35 @@ +package step.core.reporting; + +import step.core.GlobalContext; +import step.core.collections.Collection; +import step.core.entities.Entity; +import step.core.entities.EntityConstants; +import step.core.plugins.AbstractControllerPlugin; +import step.core.plugins.Plugin; +import step.framework.server.tables.Table; +import step.framework.server.tables.TableRegistry; + +import java.util.Map; + +@Plugin +public class ReportLayoutPlugin extends AbstractControllerPlugin { + + @Override + public void serverStart(GlobalContext context) throws Exception { + super.serverStart(context); + TableRegistry tableRegistry = context.require(TableRegistry.class); + //Create accessor + Collection reportLayoutCollection = context.getCollectionFactory().getCollection(EntityConstants.reportLayouts, ReportLayout.class); + ReportLayoutAccessor reportLayoutAbstractAccessor = new ReportLayoutAccessor(reportLayoutCollection); + context.put(ReportLayoutAccessor.class, reportLayoutAbstractAccessor); + //Register entity + context.getEntityManager().register(new Entity<>(EntityConstants.reportLayouts, reportLayoutAbstractAccessor, ReportLayout.class)); + //Register Table, table only return layout metadata not the layout itself + tableRegistry.register(EntityConstants.reportLayouts, new Table<>(reportLayoutCollection, ReportLayoutServices.REPORT_LAYOUT_RIGHT + "-read", false).withResultItemTransformer((reportLayout, session) -> { + reportLayout.layout = Map.of(); + return reportLayout; + })); + //Register Services + context.getServiceRegistrationCallback().registerService(ReportLayoutServices.class); + } +} diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java new file mode 100644 index 000000000..a6da42b23 --- /dev/null +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java @@ -0,0 +1,126 @@ +package step.core.reporting; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.PostConstruct; +import jakarta.inject.Singleton; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import step.controller.services.entities.AbstractEntityServices; +import step.core.deployment.AuthorizationException; +import step.core.entities.EntityConstants; +import step.framework.server.security.Secured; +import step.framework.server.security.SecuredContext; + +import java.util.List; +import java.util.Optional; + + +@Singleton +@Path("/report-layout") +@Tag(name = "ReportLayout") +@Tag(name = "Entity=ReportLayout") +@SecuredContext(key = "entity", value = ReportLayoutServices.REPORT_LAYOUT_RIGHT) +public class ReportLayoutServices extends AbstractEntityServices { + + private static final String SHARED_RIGHT_SUFFIX = "shared"; + public static final String DELETE_RIGHT = "delete"; + public static final String WRITE_RIGHT = "write"; + public static final String REPORT_LAYOUT_RIGHT = "reportLayout"; + + private ReportLayoutAccessor reportLayoutAccessor; + + public ReportLayoutServices() { + super(EntityConstants.reportLayouts); + } + + @PostConstruct + public void init() throws Exception { + super.init(); + reportLayoutAccessor = getContext().require(ReportLayoutAccessor.class); + } + + @Operation(description = "Returns all accessible report layouts.") + @GET + @Path("/list") + @Produces(MediaType.APPLICATION_JSON) + @Secured(right="{entity}-read") + public List getAllReportLayouts() { + return reportLayoutAccessor.getAccessibleReportLayoutsDefinitions(getSession().getUser().getUsername()); + } + + @Override + public ReportLayout save(ReportLayout reportLayout) { + //Only check additional specific rights when updating layout + Optional.ofNullable(get(reportLayout.getId().toHexString())).ifPresent(entity -> checkLayoutRight(entity, WRITE_RIGHT)); + return super.save(reportLayout); + } + + private void checkLayoutRight(ReportLayout reportLayout, String right) { + //If user is the owner, he is always allowed (if he has the base access right role) + //TODO user name could be modified, using ID will be required but we first need to add the ID to the tracked info, or introduce a dedicated attribute + String username = this.getSession().getUser().getUsername(); + if (!username.equals(reportLayout.getCreationUser())) { + if (reportLayout.shared) { + //Check specific access right for shared layouts + checkRights(REPORT_LAYOUT_RIGHT + "-" + right + "-" + SHARED_RIGHT_SUFFIX); + } else { + //The layout isn't shared and the current user is not the owner, forbid access + throw new AuthorizationException("This is a private layout owned by " + reportLayout.getCreationUser() + ", you have no permission to modify it."); + } + } + } + + @Override + public void delete(String id) { + ReportLayout reportLayout = getEntity(id); + checkLayoutRight(reportLayout, DELETE_RIGHT); + super.delete(id); + } + + @Override + protected ReportLayout cloneEntity(ReportLayout entity) { + ReportLayout clone = super.cloneEntity(entity); + clone.setShared(false); + return clone; + } + + @Override + public ReportLayout restoreVersion(String id, String versionId) { + ReportLayout reportLayout = getEntity(id); + checkLayoutRight(reportLayout, WRITE_RIGHT); + return super.restoreVersion(id, versionId); + } + + @Override + public void setLocked(String id, Boolean locked) { + ReportLayout reportLayout = getEntity(id); + checkLayoutRight(reportLayout, WRITE_RIGHT); + super.setLocked(id, locked); + } + + @Operation(operationId = "shareReportLayout", description = "Share this report layout with other users") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Secured(right = "{entity}-write") + @Path("{id}/share") + public void shareReportLayout(@PathParam("id") String id) { + changeSharedState(id, true); + } + + private void changeSharedState(String id, boolean share) { + ReportLayout reportLayout = getEntity(id); + reportLayout.setShared(share); + save(reportLayout); + } + + @Operation(operationId = "unshareReportLayout", description = "Unshare this report layout") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Secured(right = "{entity}-write") + @Path("{id}/unshare") + public void unshareReportLayout(@PathParam("id") String id) { + changeSharedState(id, false); + } +} diff --git a/step-core-model/src/main/java/step/core/entities/EntityConstants.java b/step-core-model/src/main/java/step/core/entities/EntityConstants.java index 677aa5c85..571fcfd31 100644 --- a/step-core-model/src/main/java/step/core/entities/EntityConstants.java +++ b/step-core-model/src/main/java/step/core/entities/EntityConstants.java @@ -14,4 +14,5 @@ public class EntityConstants { public final static String metricTypes = "metricTypes"; public final static String dashboards = "dashboards"; public final static String bookmarks = "bookmarks"; + public final static String reportLayouts = "reportLayouts"; } From e643bba3233887601b083c65b2fa296f64667e0a Mon Sep 17 00:00:00 2001 From: David Stephan Date: Mon, 9 Mar 2026 14:12:10 +0100 Subject: [PATCH 2/9] SED-4417 adding and using user IDs to verify the owner --- pom.xml | 2 +- .../services/entities/AbstractEntityServices.java | 7 ++++++- .../java/step/core/reporting/ReportLayoutServices.java | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 33fb106de..b447ce763 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 11 0.0.0-MASTER-SNAPSHOT - 0.0.0-MASTER-SNAPSHOT + 0.0.0-SED-4417-SNAPSHOT 5.0.4 diff --git a/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java b/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java index dcfe3636e..5790caef9 100644 --- a/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java +++ b/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java @@ -8,6 +8,7 @@ import step.automation.packages.AutomationPackageEntity; import step.controller.services.async.AsyncTaskStatus; import step.core.GlobalContext; +import step.core.access.User; import step.core.accessors.AbstractIdentifiableObject; import step.core.accessors.AbstractOrganizableObject; import step.core.accessors.AbstractTrackedObject; @@ -153,14 +154,18 @@ private void trackEntityIfApplicable(T entity) { AbstractTrackedObject newTrackedEntity = (AbstractTrackedObject) entity; ObjectId sourceId = entity.getId(); T sourceEntity = (sourceId != null) ? accessor.get(sourceId) : null; - String username = getSession().getUser().getUsername(); + User user = getSession().getUser(); + String username = user.getUsername(); + ObjectId userId = user.getId(); Date lastModificationDate = new Date(); if (sourceEntity == null) { newTrackedEntity.setCreationDate(lastModificationDate); newTrackedEntity.setCreationUser(username); + newTrackedEntity.setCreationUserId(userId); } newTrackedEntity.setLastModificationDate(lastModificationDate); newTrackedEntity.setLastModificationUser(username); + newTrackedEntity.setLastModificationUserId(userId); } } diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java index a6da42b23..923fad7b5 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java @@ -8,6 +8,7 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import step.controller.services.entities.AbstractEntityServices; +import step.core.access.User; import step.core.deployment.AuthorizationException; import step.core.entities.EntityConstants; import step.framework.server.security.Secured; @@ -59,9 +60,8 @@ public ReportLayout save(ReportLayout reportLayout) { private void checkLayoutRight(ReportLayout reportLayout, String right) { //If user is the owner, he is always allowed (if he has the base access right role) - //TODO user name could be modified, using ID will be required but we first need to add the ID to the tracked info, or introduce a dedicated attribute - String username = this.getSession().getUser().getUsername(); - if (!username.equals(reportLayout.getCreationUser())) { + User currentUser = this.getSession().getUser(); + if (!reportLayout.getCreationUserId().equals(currentUser.getId())) { if (reportLayout.shared) { //Check specific access right for shared layouts checkRights(REPORT_LAYOUT_RIGHT + "-" + right + "-" + SHARED_RIGHT_SUFFIX); From 12b4113d581b8cea947691b8a88056e54e713419 Mon Sep 17 00:00:00 2001 From: David Stephan Date: Tue, 10 Mar 2026 15:01:19 +0100 Subject: [PATCH 3/9] SED-4417 PR feedbacks --- .../entities/AbstractEntityServices.java | 11 ++- .../step/core/reporting/ReportLayout.java | 34 +++----- .../core/reporting/ReportLayoutAccessor.java | 14 +++- .../core/reporting/ReportLayoutPlugin.java | 30 ++++++- .../core/reporting/ReportLayoutServices.java | 81 +++++++++++++++---- .../core/reporting/presets/DefaultLayout.json | 8 ++ 6 files changed, 131 insertions(+), 47 deletions(-) create mode 100644 step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json diff --git a/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java b/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java index 5790caef9..64b890c06 100644 --- a/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java +++ b/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java @@ -156,12 +156,19 @@ private void trackEntityIfApplicable(T entity) { T sourceEntity = (sourceId != null) ? accessor.get(sourceId) : null; User user = getSession().getUser(); String username = user.getUsername(); - ObjectId userId = user.getId(); + String userId = user.getId().toHexString(); Date lastModificationDate = new Date(); - if (sourceEntity == null) { + //If source is null or not an instance of AbstractTrackedObject, we set creation metadata + if (!(sourceEntity instanceof AbstractTrackedObject)) { newTrackedEntity.setCreationDate(lastModificationDate); newTrackedEntity.setCreationUser(username); newTrackedEntity.setCreationUserId(userId); + } else { + //In case of update we make sure we keep the creation metadata from the DB info + AbstractTrackedObject sourceTrackedEntity = (AbstractTrackedObject) sourceEntity; + newTrackedEntity.setCreationDate(sourceTrackedEntity.getCreationDate()); + newTrackedEntity.setCreationUser(sourceTrackedEntity.getCreationUser()); + newTrackedEntity.setCreationUserId(sourceTrackedEntity.getCreationUserId()); } newTrackedEntity.setLastModificationDate(lastModificationDate); newTrackedEntity.setLastModificationUser(username); diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java index 69fdb8bfe..e2361cd11 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java @@ -1,34 +1,24 @@ package step.core.reporting; -import java.util.Map; - -import jakarta.validation.constraints.NotNull; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.json.JsonObject; import step.core.accessors.AbstractTrackedObject; public class ReportLayout extends AbstractTrackedObject { - public static final String FIELD_IS_SHARED = "shared"; - - @NotNull - Map layout; - boolean shared = false; - - public ReportLayout() { + public enum ReportLayoutVisibility { + Preset, Private, Shared } - public Map getLayout() { - return layout; - } + public static final String FIELD_VISIBILITY = "visibility"; - public void setLayout(Map layout) { - this.layout = layout; - } - - public boolean isShared() { - return shared; - } + public JsonObject layout; + public ReportLayoutVisibility visibility; - public void setShared(boolean shared) { - this.shared = shared; + @JsonCreator + public ReportLayout(@JsonProperty("layout") JsonObject layout, @JsonProperty(value = "visibility", defaultValue = "Private") ReportLayoutVisibility visibility) { + this.layout = layout; + this.visibility = visibility != null ? visibility : ReportLayoutVisibility.Private; } } diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java index df04c3239..01f99585e 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java @@ -12,7 +12,9 @@ import java.util.stream.Collectors; import static step.core.collections.Order.ASC; -import static step.core.reporting.ReportLayout.FIELD_IS_SHARED; +import static step.core.reporting.ReportLayout.FIELD_VISIBILITY; +import static step.core.reporting.ReportLayout.ReportLayoutVisibility.Preset; +import static step.core.reporting.ReportLayout.ReportLayoutVisibility.Shared; public class ReportLayoutAccessor extends AbstractAccessor { @@ -20,11 +22,15 @@ public ReportLayoutAccessor(Collection collectionDriver) { super(collectionDriver); } - public List getAccessibleReportLayoutsDefinitions(String username) { - Or ownerOrShared = Filters.or(List.of(Filters.equals(FIELD_IS_SHARED, true), Filters.equals("creationUser", username))); + public List getAccessibleReportLayoutsDefinitions(String userId) { + Or ownerOrShared = Filters.or(List.of(Filters.equals(FIELD_VISIBILITY, Preset.name()), Filters.equals(FIELD_VISIBILITY, Shared.name()), Filters.equals("creationUserId", userId))); return this.getCollectionDriver() .find(ownerOrShared, new SearchOrder(ATTRIBUTES_FIELD_NAME + "." + AbstractOrganizableObject.NAME, ASC.numeric), null, null, 0) - .peek(reportLayout -> reportLayout.setLayout(Map.of())) + .peek(reportLayout -> reportLayout.layout = null) .collect(Collectors.toList()); } + + public ReportLayout getReportLayoutPresetIfExists(String name) { + return findByCriteria(Map.of(ATTRIBUTES_FIELD_NAME + "." + AbstractOrganizableObject.NAME, name, FIELD_VISIBILITY, Preset.name())); + } } diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java index 035a68244..7f5bca52a 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java @@ -1,6 +1,10 @@ package step.core.reporting; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonReader; import step.core.GlobalContext; +import step.core.accessors.AbstractOrganizableObject; import step.core.collections.Collection; import step.core.entities.Entity; import step.core.entities.EntityConstants; @@ -9,11 +13,13 @@ import step.framework.server.tables.Table; import step.framework.server.tables.TableRegistry; -import java.util.Map; +import java.io.InputStream; @Plugin public class ReportLayoutPlugin extends AbstractControllerPlugin { + public static final String DEFAULT_REPORT_LAYOUT = "Default Report Layout"; + @Override public void serverStart(GlobalContext context) throws Exception { super.serverStart(context); @@ -26,10 +32,30 @@ public void serverStart(GlobalContext context) throws Exception { context.getEntityManager().register(new Entity<>(EntityConstants.reportLayouts, reportLayoutAbstractAccessor, ReportLayout.class)); //Register Table, table only return layout metadata not the layout itself tableRegistry.register(EntityConstants.reportLayouts, new Table<>(reportLayoutCollection, ReportLayoutServices.REPORT_LAYOUT_RIGHT + "-read", false).withResultItemTransformer((reportLayout, session) -> { - reportLayout.layout = Map.of(); + reportLayout.layout = null; return reportLayout; })); //Register Services context.getServiceRegistrationCallback().registerService(ReportLayoutServices.class); } + + @Override + public void initializeData(GlobalContext context) throws Exception { + super.initializeData(context); + ReportLayoutAccessor reportLayoutAccessor = context.require(ReportLayoutAccessor.class); + ReportLayout existingDefaultLayout = reportLayoutAccessor.getReportLayoutPresetIfExists(DEFAULT_REPORT_LAYOUT); + try (InputStream resourceAsStream = this.getClass().getResourceAsStream("presets/DefaultLayout.json"); + JsonReader reader = Json.createReader(resourceAsStream)) { + JsonObject layout = reader.readObject(); + ReportLayout reportLayout = new ReportLayout(layout, ReportLayout.ReportLayoutVisibility.Preset); + reportLayout.addAttribute(AbstractOrganizableObject.NAME, DEFAULT_REPORT_LAYOUT); + //Keeping same strategy as for the prepopulated dashboard, always recreate and update the existing persisted layout + if (existingDefaultLayout != null) { + reportLayout.setId(existingDefaultLayout.getId()); + } + reportLayoutAccessor.save(reportLayout); + } + + + } } diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java index 923fad7b5..f283009f8 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java @@ -8,14 +8,16 @@ import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import step.controller.services.entities.AbstractEntityServices; -import step.core.access.User; +import step.core.accessors.AbstractOrganizableObject; import step.core.deployment.AuthorizationException; import step.core.entities.EntityConstants; import step.framework.server.security.Secured; import step.framework.server.security.SecuredContext; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; @Singleton @@ -26,6 +28,7 @@ public class ReportLayoutServices extends AbstractEntityServices { private static final String SHARED_RIGHT_SUFFIX = "shared"; + public static final String READ_RIGHT = "read"; public static final String DELETE_RIGHT = "delete"; public static final String WRITE_RIGHT = "write"; public static final String REPORT_LAYOUT_RIGHT = "reportLayout"; @@ -42,32 +45,76 @@ public void init() throws Exception { reportLayoutAccessor = getContext().require(ReportLayoutAccessor.class); } + @Override + public ReportLayout get(String id) { + ReportLayout reportLayout = super.get(id); + checkLayoutRight(reportLayout, READ_RIGHT); + return reportLayout; + } + + @Override + public List findByIds(List ids) { + List byIds = super.findByIds(ids); + byIds.forEach(r -> checkLayoutRight(r, READ_RIGHT)); + return byIds; + } + + @Override + public Map findNamesByIds(List ids) { + return reportLayoutAccessor.findByIds(ids).peek(l -> checkLayoutRight(l, READ_RIGHT)).collect(Collectors.toMap(a -> a.getId().toHexString(), a -> + a.getAttribute(AbstractOrganizableObject.NAME) + )); + } + + @Override + public List findManyByAttributes(Map attributes) { + return super.findManyByAttributes(attributes).stream().filter(this::canReadLayout).collect(Collectors.toList()); + } + + private boolean canReadLayout(ReportLayout r) { + try { + checkLayoutRight(r, READ_RIGHT); + return true; + } catch (AuthorizationException e) { + return false; + } + } + @Operation(description = "Returns all accessible report layouts.") @GET @Path("/list") @Produces(MediaType.APPLICATION_JSON) @Secured(right="{entity}-read") public List getAllReportLayouts() { - return reportLayoutAccessor.getAccessibleReportLayoutsDefinitions(getSession().getUser().getUsername()); + return reportLayoutAccessor.getAccessibleReportLayoutsDefinitions(getSession().getUser().getId().toHexString()); } @Override public ReportLayout save(ReportLayout reportLayout) { //Only check additional specific rights when updating layout - Optional.ofNullable(get(reportLayout.getId().toHexString())).ifPresent(entity -> checkLayoutRight(entity, WRITE_RIGHT)); + Optional.ofNullable(super.get(reportLayout.getId().toHexString())).ifPresent(entity -> checkLayoutRight(entity, WRITE_RIGHT)); return super.save(reportLayout); } private void checkLayoutRight(ReportLayout reportLayout, String right) { - //If user is the owner, he is always allowed (if he has the base access right role) - User currentUser = this.getSession().getUser(); - if (!reportLayout.getCreationUserId().equals(currentUser.getId())) { - if (reportLayout.shared) { - //Check specific access right for shared layouts - checkRights(REPORT_LAYOUT_RIGHT + "-" + right + "-" + SHARED_RIGHT_SUFFIX); - } else { - //The layout isn't shared and the current user is not the owner, forbid access - throw new AuthorizationException("This is a private layout owned by " + reportLayout.getCreationUser() + ", you have no permission to modify it."); + if (ReportLayout.ReportLayoutVisibility.Preset.equals(reportLayout.visibility)) { + //Preset layouts can be read by any user with the base right, modifications are never allowed + if (!READ_RIGHT.equals(right)) { + throw new AuthorizationException("Modifying a preset layout is not allowed."); + } + } else { + //If the current user is the owner, he is always allowed (if he has the base access right role) + if (!reportLayout.getCreationUserId().equals(this.getSession().getUser().getId().toHexString())) { + if (ReportLayout.ReportLayoutVisibility.Shared.equals(reportLayout.visibility)) { + //Base read right automatically grant access to reading shared dashboard, write and delete require specific rights + if (!READ_RIGHT.equals(right)) { + //Check specific access right for shared layouts + checkRights(REPORT_LAYOUT_RIGHT + "-" + SHARED_RIGHT_SUFFIX + "-" + right); + } + } else { + //The layout isn't shared, the current user is not the owner, and he doesn't have the "all" right + throw new AuthorizationException("This is a private layout owned by " + reportLayout.getCreationUser() + ", you have no permission to " + right + " it."); + } } } } @@ -82,7 +129,7 @@ public void delete(String id) { @Override protected ReportLayout cloneEntity(ReportLayout entity) { ReportLayout clone = super.cloneEntity(entity); - clone.setShared(false); + clone.visibility = ReportLayout.ReportLayoutVisibility.Private; return clone; } @@ -106,12 +153,12 @@ public void setLocked(String id, Boolean locked) { @Secured(right = "{entity}-write") @Path("{id}/share") public void shareReportLayout(@PathParam("id") String id) { - changeSharedState(id, true); + changeLayoutVisibility(id, ReportLayout.ReportLayoutVisibility.Shared); } - private void changeSharedState(String id, boolean share) { + private void changeLayoutVisibility(String id, ReportLayout.ReportLayoutVisibility visibility) { ReportLayout reportLayout = getEntity(id); - reportLayout.setShared(share); + reportLayout.visibility = visibility; save(reportLayout); } @@ -121,6 +168,6 @@ private void changeSharedState(String id, boolean share) { @Secured(right = "{entity}-write") @Path("{id}/unshare") public void unshareReportLayout(@PathParam("id") String id) { - changeSharedState(id, false); + changeLayoutVisibility(id, ReportLayout.ReportLayoutVisibility.Private); } } diff --git a/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json b/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json new file mode 100644 index 000000000..8f0cf954f --- /dev/null +++ b/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json @@ -0,0 +1,8 @@ +{ + "keyString": "valueString", + "nested": { + "nestedKey": "nestedValue" + }, + "booleanKey": true, + "numberKey": 1 +} \ No newline at end of file From c68fcdbb28212c215e5709cb49d0e2033ca8e2c4 Mon Sep 17 00:00:00 2001 From: David Stephan Date: Tue, 10 Mar 2026 15:56:36 +0100 Subject: [PATCH 4/9] SED-4417 adding FE default layout --- .../core/reporting/ReportLayoutPlugin.java | 2 +- .../core/reporting/presets/DefaultLayout.json | 121 +++++++++++++++++- 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java index 7f5bca52a..939e16fbe 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java @@ -18,7 +18,7 @@ @Plugin public class ReportLayoutPlugin extends AbstractControllerPlugin { - public static final String DEFAULT_REPORT_LAYOUT = "Default Report Layout"; + public static final String DEFAULT_REPORT_LAYOUT = "Default"; @Override public void serverStart(GlobalContext context) throws Exception { diff --git a/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json b/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json index 8f0cf954f..c63f2d1bf 100644 --- a/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json +++ b/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json @@ -1,8 +1,117 @@ { - "keyString": "valueString", - "nested": { - "nestedKey": "nestedValue" - }, - "booleanKey": true, - "numberKey": 1 + "id": "default", + "name": "Default", + "protected": true, + "widgets": [ + { + "id": "c4460688-41d9-4765-a260-d3bed46c4d50", + "widgetType": "errorsWidget", + "position": { + "row": 1, + "column": 1, + "widthInCells": 8, + "heightInCells": 1 + } + }, + { + "id": "c11a02b1-de67-408a-9abe-f036f3ba7894", + "widgetType": "testCases", + "position": { + "row": 2, + "column": 1, + "widthInCells": 6, + "heightInCells": 3 + } + }, + { + "id": "eb2af1f3-1037-409d-943f-35434a2cb0ed", + "widgetType": "testCasesSummary", + "position": { + "row": 2, + "column": 7, + "widthInCells": 2, + "heightInCells": 3 + } + }, + { + "id": "8556aa3f-87fb-4300-b1e4-c7da0f7bbc9c", + "widgetType": "keywordsSummary", + "position": { + "row": 5, + "column": 1, + "widthInCells": 2, + "heightInCells": 3 + } + }, + { + "id": "505e9f33-c342-40de-b390-01812dd6a2f7", + "widgetType": "executionTree", + "position": { + "row": 5, + "column": 3, + "widthInCells": 6, + "heightInCells": 3 + } + }, + { + "id": "a1928775-e4de-42e1-aab6-74e357462c47", + "widgetType": "keywordsList", + "position": { + "row": 8, + "column": 1, + "widthInCells": 6, + "heightInCells": 3 + } + }, + { + "id": "4cd5b4d3-f12e-43e2-bb91-7609d89098c7", + "widgetType": "performanceOverview", + "position": { + "row": 8, + "column": 7, + "widthInCells": 2, + "heightInCells": 3 + } + }, + { + "id": "ef3f70e8-eed9-433d-ba07-c6c830eb1122", + "widgetType": "errors", + "position": { + "row": 11, + "column": 1, + "widthInCells": 8, + "heightInCells": 3 + } + }, + { + "id": "d0d7bfd9-1d2e-4170-9fa1-a62000073d9f", + "widgetType": "notificationSubscriptionForExecution", + "position": { + "row": 14, + "column": 1, + "widthInCells": 4, + "heightInCells": 3 + } + }, + { + "id": "7da124f6-89ba-42b6-a888-1502a784641f", + "widgetType": "housekeepingSettingsForExecution", + "position": { + "row": 14, + "column": 5, + "widthInCells": 4, + "heightInCells": 3 + } + }, + { + "id": "11af33b5-48b9-46ba-9f9f-d9feb7ef2756", + "widgetType": "currentOperations", + "position": { + "row": 17, + "column": 1, + "widthInCells": 4, + "heightInCells": 3 + } + } + ] } \ No newline at end of file From bf259cae3a52dadbac1bfeb34ebc32c9da6d8912 Mon Sep 17 00:00:00 2001 From: David Stephan Date: Tue, 10 Mar 2026 16:00:34 +0100 Subject: [PATCH 5/9] SED-4417 adding FE default layout --- .../resources/step/core/reporting/presets/DefaultLayout.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json b/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json index c63f2d1bf..950dac16e 100644 --- a/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json +++ b/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json @@ -1,7 +1,4 @@ { - "id": "default", - "name": "Default", - "protected": true, "widgets": [ { "id": "c4460688-41d9-4765-a260-d3bed46c4d50", From 75bf5bc51e04bba18348d5ece6834ddc120d832e Mon Sep 17 00:00:00 2001 From: David Stephan Date: Wed, 11 Mar 2026 09:11:04 +0100 Subject: [PATCH 6/9] SED-4417 merging master code reformatting --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 33f7f3b41..b0f5e1451 100644 --- a/pom.xml +++ b/pom.xml @@ -46,7 +46,7 @@ 11 0.0.0-MASTER-SNAPSHOT - 0.0.0-SED-4417-SNAPSHOT + 0.0.0-SED-4417-SNAPSHOT 5.0.4 From 1828216a12ff5f796900088b6d5b0f8d6badf4bb Mon Sep 17 00:00:00 2001 From: David Stephan Date: Thu, 12 Mar 2026 14:30:01 +0100 Subject: [PATCH 7/9] SED-4417 Refactoring, relaying on username, loading presets from filesystem --- .gitignore | 3 +- .../layouts/presets/StepMainReportLayout.json | 118 +++++++ .../src/test/resources/step.properties | 5 + .../entities/AbstractEntityServices.java | 3 - .../core/reporting/ReportLayoutAccessor.java | 15 +- .../core/reporting/ReportLayoutPlugin.java | 78 +++-- .../core/reporting/ReportLayoutServices.java | 17 +- .../reporting/{ => model}/ReportLayout.java | 4 +- .../reporting/model/ReportLayoutJson.java | 26 ++ .../core/reporting/presets/DefaultLayout.json | 114 ------ .../reporting/ReportLayoutAccessorTest.java | 107 ++++++ .../reporting/ReportLayoutPluginTest.java | 327 ++++++++++++++++++ 12 files changed, 666 insertions(+), 151 deletions(-) create mode 100644 step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json rename step-controller/step-controller-server/src/main/java/step/core/reporting/{ => model}/ReportLayout.java (82%) create mode 100644 step-controller/step-controller-server/src/main/java/step/core/reporting/model/ReportLayoutJson.java delete mode 100644 step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json create mode 100644 step-controller/step-controller-server/src/test/java/step/core/reporting/ReportLayoutAccessorTest.java create mode 100644 step-controller/step-controller-server/src/test/java/step/core/reporting/ReportLayoutPluginTest.java diff --git a/.gitignore b/.gitignore index 973d37762..40f1e4fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ step-controller/step-controller-base-plugins/resources/ /step-cli/step-cli-launcher/src/test/resources/samples/ work/ /step-cli/step-cli-core/src/test/resources/testReports/ -**/.claude \ No newline at end of file +dependency-reduced-pom.xml +**/.claude diff --git a/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json b/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json new file mode 100644 index 000000000..f678c56ef --- /dev/null +++ b/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json @@ -0,0 +1,118 @@ +{ + "uid": "fe687d84-5414-4fa0-ba2e-3bd09fef11b3", + "name": "Step Main Layout", + "layout": { + "widgets": [ + { + "id": "c4460688-41d9-4765-a260-d3bed46c4d50", + "widgetType": "errorsWidget", + "position": { + "row": 1, + "column": 1, + "widthInCells": 8, + "heightInCells": 1 + } + }, + { + "id": "c11a02b1-de67-408a-9abe-f036f3ba7894", + "widgetType": "testCases", + "position": { + "row": 2, + "column": 1, + "widthInCells": 6, + "heightInCells": 3 + } + }, + { + "id": "eb2af1f3-1037-409d-943f-35434a2cb0ed", + "widgetType": "testCasesSummary", + "position": { + "row": 2, + "column": 7, + "widthInCells": 2, + "heightInCells": 3 + } + }, + { + "id": "8556aa3f-87fb-4300-b1e4-c7da0f7bbc9c", + "widgetType": "keywordsSummary", + "position": { + "row": 5, + "column": 1, + "widthInCells": 2, + "heightInCells": 3 + } + }, + { + "id": "505e9f33-c342-40de-b390-01812dd6a2f7", + "widgetType": "executionTree", + "position": { + "row": 5, + "column": 3, + "widthInCells": 6, + "heightInCells": 3 + } + }, + { + "id": "a1928775-e4de-42e1-aab6-74e357462c47", + "widgetType": "keywordsList", + "position": { + "row": 8, + "column": 1, + "widthInCells": 6, + "heightInCells": 3 + } + }, + { + "id": "4cd5b4d3-f12e-43e2-bb91-7609d89098c7", + "widgetType": "performanceOverview", + "position": { + "row": 8, + "column": 7, + "widthInCells": 2, + "heightInCells": 3 + } + }, + { + "id": "ef3f70e8-eed9-433d-ba07-c6c830eb1122", + "widgetType": "errors", + "position": { + "row": 11, + "column": 1, + "widthInCells": 8, + "heightInCells": 3 + } + }, + { + "id": "d0d7bfd9-1d2e-4170-9fa1-a62000073d9f", + "widgetType": "notificationSubscriptionForExecution", + "position": { + "row": 14, + "column": 1, + "widthInCells": 4, + "heightInCells": 3 + } + }, + { + "id": "7da124f6-89ba-42b6-a888-1502a784641f", + "widgetType": "housekeepingSettingsForExecution", + "position": { + "row": 14, + "column": 5, + "widthInCells": 4, + "heightInCells": 3 + } + }, + { + "id": "11af33b5-48b9-46ba-9f9f-d9feb7ef2756", + "widgetType": "currentOperations", + "position": { + "row": 17, + "column": 1, + "widthInCells": 4, + "heightInCells": 3 + } + } + ] + } +} diff --git a/step-controller/step-controller-backend/src/test/resources/step.properties b/step-controller/step-controller-backend/src/test/resources/step.properties index ed348bbe9..f8f7b5870 100644 --- a/step-controller/step-controller-backend/src/test/resources/step.properties +++ b/step-controller/step-controller-backend/src/test/resources/step.properties @@ -94,3 +94,8 @@ plugins.FunctionPackagePlugin.maven.repository.central.url=https://repo1.maven.o #timeseries.collections.week.flush.period=3600000 #timeseries.response.intervals.ideal=100 #timeseries.response.intervals.max=1000 +#----------------------------------------------------- +# Reporting +#----------------------------------------------------- +plugins.reporting.layouts.presets.folder=src/test/resources/reporting/layouts/presets +plugins.reporting.layouts.default.id=69b010aeec94534eb48176db diff --git a/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java b/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java index 7a9ce44b8..ebce0ca74 100644 --- a/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java +++ b/step-controller/step-controller-server/src/main/java/step/controller/services/entities/AbstractEntityServices.java @@ -161,17 +161,14 @@ private void trackEntityIfApplicable(T entity) { if (!(sourceEntity instanceof AbstractTrackedObject)) { newTrackedEntity.setCreationDate(lastModificationDate); newTrackedEntity.setCreationUser(username); - newTrackedEntity.setCreationUserId(userId); } else { //In case of update we make sure we keep the creation metadata from the DB info AbstractTrackedObject sourceTrackedEntity = (AbstractTrackedObject) sourceEntity; newTrackedEntity.setCreationDate(sourceTrackedEntity.getCreationDate()); newTrackedEntity.setCreationUser(sourceTrackedEntity.getCreationUser()); - newTrackedEntity.setCreationUserId(sourceTrackedEntity.getCreationUserId()); } newTrackedEntity.setLastModificationDate(lastModificationDate); newTrackedEntity.setLastModificationUser(username); - newTrackedEntity.setLastModificationUserId(userId); } } diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java index 01f99585e..009b9fedf 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutAccessor.java @@ -6,15 +6,15 @@ import step.core.collections.Filters; import step.core.collections.SearchOrder; import step.core.collections.filters.Or; +import step.core.reporting.model.ReportLayout; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; import static step.core.collections.Order.ASC; -import static step.core.reporting.ReportLayout.FIELD_VISIBILITY; -import static step.core.reporting.ReportLayout.ReportLayoutVisibility.Preset; -import static step.core.reporting.ReportLayout.ReportLayoutVisibility.Shared; +import static step.core.reporting.model.ReportLayout.FIELD_VISIBILITY; +import static step.core.reporting.model.ReportLayout.ReportLayoutVisibility.Preset; +import static step.core.reporting.model.ReportLayout.ReportLayoutVisibility.Shared; public class ReportLayoutAccessor extends AbstractAccessor { @@ -22,15 +22,12 @@ public ReportLayoutAccessor(Collection collectionDriver) { super(collectionDriver); } - public List getAccessibleReportLayoutsDefinitions(String userId) { - Or ownerOrShared = Filters.or(List.of(Filters.equals(FIELD_VISIBILITY, Preset.name()), Filters.equals(FIELD_VISIBILITY, Shared.name()), Filters.equals("creationUserId", userId))); + public List getAccessibleReportLayoutsDefinitions(String userName) { + Or ownerOrShared = Filters.or(List.of(Filters.equals(FIELD_VISIBILITY, Preset.name()), Filters.equals(FIELD_VISIBILITY, Shared.name()), Filters.equals("creationUser", userName))); return this.getCollectionDriver() .find(ownerOrShared, new SearchOrder(ATTRIBUTES_FIELD_NAME + "." + AbstractOrganizableObject.NAME, ASC.numeric), null, null, 0) .peek(reportLayout -> reportLayout.layout = null) .collect(Collectors.toList()); } - public ReportLayout getReportLayoutPresetIfExists(String name) { - return findByCriteria(Map.of(ATTRIBUTES_FIELD_NAME + "." + AbstractOrganizableObject.NAME, name, FIELD_VISIBILITY, Preset.name())); - } } diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java index 939e16fbe..ffddf3845 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutPlugin.java @@ -1,24 +1,42 @@ package step.core.reporting; -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.json.JsonReader; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.bson.types.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import step.core.GlobalContext; import step.core.accessors.AbstractOrganizableObject; +import step.core.accessors.DefaultJacksonMapperProvider; import step.core.collections.Collection; +import step.core.collections.Filters; +import step.core.deployment.WebApplicationConfigurationManager; import step.core.entities.Entity; import step.core.entities.EntityConstants; import step.core.plugins.AbstractControllerPlugin; import step.core.plugins.Plugin; +import step.core.reporting.model.ReportLayout; +import step.core.reporting.model.ReportLayoutJson; import step.framework.server.tables.Table; import step.framework.server.tables.TableRegistry; -import java.io.InputStream; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; @Plugin public class ReportLayoutPlugin extends AbstractControllerPlugin { + private static final Logger logger = LoggerFactory.getLogger(ReportLayoutPlugin.class); + public static final String DEFAULT_REPORT_LAYOUT = "Default"; + public static final String PRESET_FOLDER_PATH_CONFIG_KEY = "plugins.reporting.layouts.presets.folder"; + public static final String PRESET_FOLDER_PATH_DEFAULT = "../plugins/reporting/layouts"; + public static final String DEFAULT_LAYOUT_ID_CONFIG_KEY = "plugins.reporting.layouts.default.id"; + public static final String DEFAULT_LAYOUT_ID_DEFAULT = "69b010aeec94534eb48176db"; + + private ReportLayoutAccessor reportLayoutAccessor; + private String defaultLayoutId; @Override public void serverStart(GlobalContext context) throws Exception { @@ -26,10 +44,10 @@ public void serverStart(GlobalContext context) throws Exception { TableRegistry tableRegistry = context.require(TableRegistry.class); //Create accessor Collection reportLayoutCollection = context.getCollectionFactory().getCollection(EntityConstants.reportLayouts, ReportLayout.class); - ReportLayoutAccessor reportLayoutAbstractAccessor = new ReportLayoutAccessor(reportLayoutCollection); - context.put(ReportLayoutAccessor.class, reportLayoutAbstractAccessor); + reportLayoutAccessor = new ReportLayoutAccessor(reportLayoutCollection); + context.put(ReportLayoutAccessor.class, reportLayoutAccessor); //Register entity - context.getEntityManager().register(new Entity<>(EntityConstants.reportLayouts, reportLayoutAbstractAccessor, ReportLayout.class)); + context.getEntityManager().register(new Entity<>(EntityConstants.reportLayouts, reportLayoutAccessor, ReportLayout.class)); //Register Table, table only return layout metadata not the layout itself tableRegistry.register(EntityConstants.reportLayouts, new Table<>(reportLayoutCollection, ReportLayoutServices.REPORT_LAYOUT_RIGHT + "-read", false).withResultItemTransformer((reportLayout, session) -> { reportLayout.layout = null; @@ -37,25 +55,45 @@ public void serverStart(GlobalContext context) throws Exception { })); //Register Services context.getServiceRegistrationCallback().registerService(ReportLayoutServices.class); + + //Add default layout ID to UI configuration + defaultLayoutId = context.getConfiguration().getProperty(DEFAULT_LAYOUT_ID_CONFIG_KEY, DEFAULT_LAYOUT_ID_DEFAULT); + WebApplicationConfigurationManager configurationManager = context.require(WebApplicationConfigurationManager.class); + configurationManager.registerHook(s -> Map.of(DEFAULT_LAYOUT_ID_CONFIG_KEY, defaultLayoutId)); + } @Override public void initializeData(GlobalContext context) throws Exception { super.initializeData(context); - ReportLayoutAccessor reportLayoutAccessor = context.require(ReportLayoutAccessor.class); - ReportLayout existingDefaultLayout = reportLayoutAccessor.getReportLayoutPresetIfExists(DEFAULT_REPORT_LAYOUT); - try (InputStream resourceAsStream = this.getClass().getResourceAsStream("presets/DefaultLayout.json"); - JsonReader reader = Json.createReader(resourceAsStream)) { - JsonObject layout = reader.readObject(); - ReportLayout reportLayout = new ReportLayout(layout, ReportLayout.ReportLayoutVisibility.Preset); - reportLayout.addAttribute(AbstractOrganizableObject.NAME, DEFAULT_REPORT_LAYOUT); - //Keeping same strategy as for the prepopulated dashboard, always recreate and update the existing persisted layout - if (existingDefaultLayout != null) { - reportLayout.setId(existingDefaultLayout.getId()); + // Drop all existing presets - the folder is the source of truth at startup + reportLayoutAccessor.getCollectionDriver().remove( + Filters.equals(ReportLayout.FIELD_VISIBILITY, ReportLayout.ReportLayoutVisibility.Preset.name())); + + // Load presets from the configured folder + File presetsFolder = new File(context.getConfiguration().getProperty(PRESET_FOLDER_PATH_CONFIG_KEY, PRESET_FOLDER_PATH_DEFAULT)); + if (presetsFolder.exists() && presetsFolder.isDirectory()) { + ObjectMapper objectMapper = DefaultJacksonMapperProvider.getObjectMapper(); + File[] jsonFiles = presetsFolder.listFiles((dir, name) -> name.endsWith(".json")); + if (jsonFiles != null) { + for (File jsonFile : jsonFiles) { + try { + ReportLayoutJson layoutJson = objectMapper.readValue(jsonFile, ReportLayoutJson.class); + if (ObjectId.isValid(layoutJson.id)) { + ReportLayout reportLayout = new ReportLayout(layoutJson.layout, ReportLayout.ReportLayoutVisibility.Preset); + reportLayout.addAttribute(AbstractOrganizableObject.NAME, layoutJson.name); + reportLayout.setId(new ObjectId(layoutJson.id)); + reportLayoutAccessor.save(reportLayout); + } else { + logger.error("Invalid json file: {}, the id {} has been tempered with and is not a valid ObjectId", jsonFile.getAbsolutePath(), layoutJson.id); + } + } catch (Exception e) { + logger.error("Failed to load preset layout from file '{}'", jsonFile.getAbsolutePath(), e); + } + } } - reportLayoutAccessor.save(reportLayout); + } else { + logger.warn("The configured presets folder '{}' does not exist or is not a directory.", presetsFolder.getAbsolutePath()); } - - } } diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java index f283009f8..cc71fce42 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java @@ -7,10 +7,13 @@ import jakarta.inject.Singleton; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; +import org.apache.poi.ss.formula.functions.T; import step.controller.services.entities.AbstractEntityServices; import step.core.accessors.AbstractOrganizableObject; import step.core.deployment.AuthorizationException; import step.core.entities.EntityConstants; +import step.core.reporting.model.ReportLayout; +import step.core.reporting.model.ReportLayoutJson; import step.framework.server.security.Secured; import step.framework.server.security.SecuredContext; @@ -52,6 +55,16 @@ public ReportLayout get(String id) { return reportLayout; } + @Operation(operationId = "exportLayout", description = "Export the report layout to its Json representation") + @GET + @Path("/{id}/json") + @Produces(MediaType.APPLICATION_JSON) + @Secured(right = "{entity}-read") + public ReportLayoutJson exportLayout(@PathParam("id") String id) { + ReportLayout reportLayout = getEntity(id); + return new ReportLayoutJson(reportLayout); + } + @Override public List findByIds(List ids) { List byIds = super.findByIds(ids); @@ -86,7 +99,7 @@ private boolean canReadLayout(ReportLayout r) { @Produces(MediaType.APPLICATION_JSON) @Secured(right="{entity}-read") public List getAllReportLayouts() { - return reportLayoutAccessor.getAccessibleReportLayoutsDefinitions(getSession().getUser().getId().toHexString()); + return reportLayoutAccessor.getAccessibleReportLayoutsDefinitions(getSession().getUser().getUsername()); } @Override @@ -104,7 +117,7 @@ private void checkLayoutRight(ReportLayout reportLayout, String right) { } } else { //If the current user is the owner, he is always allowed (if he has the base access right role) - if (!reportLayout.getCreationUserId().equals(this.getSession().getUser().getId().toHexString())) { + if (!reportLayout.getCreationUser().equals(this.getSession().getUser().getUsername())) { if (ReportLayout.ReportLayoutVisibility.Shared.equals(reportLayout.visibility)) { //Base read right automatically grant access to reading shared dashboard, write and delete require specific rights if (!READ_RIGHT.equals(right)) { diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/model/ReportLayout.java similarity index 82% rename from step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java rename to step-controller/step-controller-server/src/main/java/step/core/reporting/model/ReportLayout.java index e2361cd11..5f35b59b3 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayout.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/model/ReportLayout.java @@ -1,4 +1,4 @@ -package step.core.reporting; +package step.core.reporting.model; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -17,7 +17,7 @@ public enum ReportLayoutVisibility { public ReportLayoutVisibility visibility; @JsonCreator - public ReportLayout(@JsonProperty("layout") JsonObject layout, @JsonProperty(value = "visibility", defaultValue = "Private") ReportLayoutVisibility visibility) { + public ReportLayout(@JsonProperty("layout") JsonObject layout, @JsonProperty(value = FIELD_VISIBILITY, defaultValue = "Private") ReportLayoutVisibility visibility) { this.layout = layout; this.visibility = visibility != null ? visibility : ReportLayoutVisibility.Private; } diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/model/ReportLayoutJson.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/model/ReportLayoutJson.java new file mode 100644 index 000000000..c5af5b7f6 --- /dev/null +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/model/ReportLayoutJson.java @@ -0,0 +1,26 @@ +package step.core.reporting.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.json.JsonObject; +import step.core.accessors.AbstractOrganizableObject; + +public class ReportLayoutJson { + + public final String id; + public final String name; + public final JsonObject layout; + + @JsonCreator + public ReportLayoutJson(@JsonProperty("id") String id, @JsonProperty("name") String name, @JsonProperty("layout") JsonObject layout) { + this.id = id; + this.name = name; + this.layout = layout; + } + + public ReportLayoutJson(ReportLayout reportLayout) { + this.id = reportLayout.getId().toString(); + this.name = reportLayout.getAttribute(AbstractOrganizableObject.NAME); + this.layout = reportLayout.layout; + } +} diff --git a/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json b/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json deleted file mode 100644 index 950dac16e..000000000 --- a/step-controller/step-controller-server/src/main/resources/step/core/reporting/presets/DefaultLayout.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "widgets": [ - { - "id": "c4460688-41d9-4765-a260-d3bed46c4d50", - "widgetType": "errorsWidget", - "position": { - "row": 1, - "column": 1, - "widthInCells": 8, - "heightInCells": 1 - } - }, - { - "id": "c11a02b1-de67-408a-9abe-f036f3ba7894", - "widgetType": "testCases", - "position": { - "row": 2, - "column": 1, - "widthInCells": 6, - "heightInCells": 3 - } - }, - { - "id": "eb2af1f3-1037-409d-943f-35434a2cb0ed", - "widgetType": "testCasesSummary", - "position": { - "row": 2, - "column": 7, - "widthInCells": 2, - "heightInCells": 3 - } - }, - { - "id": "8556aa3f-87fb-4300-b1e4-c7da0f7bbc9c", - "widgetType": "keywordsSummary", - "position": { - "row": 5, - "column": 1, - "widthInCells": 2, - "heightInCells": 3 - } - }, - { - "id": "505e9f33-c342-40de-b390-01812dd6a2f7", - "widgetType": "executionTree", - "position": { - "row": 5, - "column": 3, - "widthInCells": 6, - "heightInCells": 3 - } - }, - { - "id": "a1928775-e4de-42e1-aab6-74e357462c47", - "widgetType": "keywordsList", - "position": { - "row": 8, - "column": 1, - "widthInCells": 6, - "heightInCells": 3 - } - }, - { - "id": "4cd5b4d3-f12e-43e2-bb91-7609d89098c7", - "widgetType": "performanceOverview", - "position": { - "row": 8, - "column": 7, - "widthInCells": 2, - "heightInCells": 3 - } - }, - { - "id": "ef3f70e8-eed9-433d-ba07-c6c830eb1122", - "widgetType": "errors", - "position": { - "row": 11, - "column": 1, - "widthInCells": 8, - "heightInCells": 3 - } - }, - { - "id": "d0d7bfd9-1d2e-4170-9fa1-a62000073d9f", - "widgetType": "notificationSubscriptionForExecution", - "position": { - "row": 14, - "column": 1, - "widthInCells": 4, - "heightInCells": 3 - } - }, - { - "id": "7da124f6-89ba-42b6-a888-1502a784641f", - "widgetType": "housekeepingSettingsForExecution", - "position": { - "row": 14, - "column": 5, - "widthInCells": 4, - "heightInCells": 3 - } - }, - { - "id": "11af33b5-48b9-46ba-9f9f-d9feb7ef2756", - "widgetType": "currentOperations", - "position": { - "row": 17, - "column": 1, - "widthInCells": 4, - "heightInCells": 3 - } - } - ] -} \ No newline at end of file diff --git a/step-controller/step-controller-server/src/test/java/step/core/reporting/ReportLayoutAccessorTest.java b/step-controller/step-controller-server/src/test/java/step/core/reporting/ReportLayoutAccessorTest.java new file mode 100644 index 000000000..fce675d4c --- /dev/null +++ b/step-controller/step-controller-server/src/test/java/step/core/reporting/ReportLayoutAccessorTest.java @@ -0,0 +1,107 @@ +package step.core.reporting; + +import org.junit.Before; +import org.junit.Test; +import step.core.accessors.AbstractOrganizableObject; +import step.core.collections.inmemory.InMemoryCollectionFactory; +import step.core.reporting.model.ReportLayout; + +import java.util.List; +import java.util.Properties; + +import static org.junit.Assert.*; + +public class ReportLayoutAccessorTest { + + private ReportLayoutAccessor accessor; + + @Before + public void setUp() { + InMemoryCollectionFactory factory = new InMemoryCollectionFactory(new Properties()); + accessor = new ReportLayoutAccessor(factory.getCollection("reportLayouts", ReportLayout.class)); + } + + private ReportLayout saveLayout(String name, ReportLayout.ReportLayoutVisibility visibility, String user) { + ReportLayout layout = new ReportLayout(null, visibility); + layout.addAttribute(AbstractOrganizableObject.NAME, name); + layout.setCreationUser(user); + return accessor.save(layout); + } + + // --- getAccessibleReportLayoutsDefinitions --- + + @Test + public void getAccessibleReportLayoutsDefinitions_includesPresets() { + saveLayout("GlobalPreset", ReportLayout.ReportLayoutVisibility.Preset, null); + + List layouts = accessor.getAccessibleReportLayoutsDefinitions("anyUser"); + + assertEquals(1, layouts.size()); + assertEquals("GlobalPreset", layouts.get(0).getAttribute(AbstractOrganizableObject.NAME)); + } + + @Test + public void getAccessibleReportLayoutsDefinitions_includesSharedLayouts() { + saveLayout("SharedLayout", ReportLayout.ReportLayoutVisibility.Shared, "alice"); + + List layouts = accessor.getAccessibleReportLayoutsDefinitions("bob"); + + assertEquals(1, layouts.size()); + assertEquals("SharedLayout", layouts.get(0).getAttribute(AbstractOrganizableObject.NAME)); + } + + @Test + public void getAccessibleReportLayoutsDefinitions_includesOwnPrivateLayout() { + saveLayout("AlicePrivate", ReportLayout.ReportLayoutVisibility.Private, "alice"); + + List layouts = accessor.getAccessibleReportLayoutsDefinitions("alice"); + + assertEquals(1, layouts.size()); + assertEquals("AlicePrivate", layouts.get(0).getAttribute(AbstractOrganizableObject.NAME)); + } + + @Test + public void getAccessibleReportLayoutsDefinitions_excludesOtherUsersPrivateLayout() { + saveLayout("AlicePrivate", ReportLayout.ReportLayoutVisibility.Private, "alice"); + + List layouts = accessor.getAccessibleReportLayoutsDefinitions("bob"); + + assertTrue(layouts.isEmpty()); + } + + @Test + public void getAccessibleReportLayoutsDefinitions_combinesAllVisibleTypes() { + saveLayout("Preset", ReportLayout.ReportLayoutVisibility.Preset, null); + saveLayout("Shared", ReportLayout.ReportLayoutVisibility.Shared, "alice"); + saveLayout("BobPrivate", ReportLayout.ReportLayoutVisibility.Private, "bob"); + saveLayout("AlicePrivate", ReportLayout.ReportLayoutVisibility.Private, "alice"); + + List layouts = accessor.getAccessibleReportLayoutsDefinitions("bob"); + + assertEquals(3, layouts.size()); // preset + shared + bob's own private + } + + @Test + public void getAccessibleReportLayoutsDefinitions_stripsLayoutField() { + saveLayout("Preset", ReportLayout.ReportLayoutVisibility.Preset, null); + saveLayout("OwnPrivate", ReportLayout.ReportLayoutVisibility.Private, "alice"); + + List layouts = accessor.getAccessibleReportLayoutsDefinitions("alice"); + + layouts.forEach(l -> assertNull("layout field should be stripped", l.layout)); + } + + @Test + public void getAccessibleReportLayoutsDefinitions_sortedByName() { + saveLayout("Z Layout", ReportLayout.ReportLayoutVisibility.Preset, null); + saveLayout("A Layout", ReportLayout.ReportLayoutVisibility.Preset, null); + saveLayout("M Layout", ReportLayout.ReportLayoutVisibility.Preset, null); + + List layouts = accessor.getAccessibleReportLayoutsDefinitions("anyUser"); + + assertEquals(3, layouts.size()); + assertEquals("A Layout", layouts.get(0).getAttribute(AbstractOrganizableObject.NAME)); + assertEquals("M Layout", layouts.get(1).getAttribute(AbstractOrganizableObject.NAME)); + assertEquals("Z Layout", layouts.get(2).getAttribute(AbstractOrganizableObject.NAME)); + } +} diff --git a/step-controller/step-controller-server/src/test/java/step/core/reporting/ReportLayoutPluginTest.java b/step-controller/step-controller-server/src/test/java/step/core/reporting/ReportLayoutPluginTest.java new file mode 100644 index 000000000..86d95f848 --- /dev/null +++ b/step-controller/step-controller-server/src/test/java/step/core/reporting/ReportLayoutPluginTest.java @@ -0,0 +1,327 @@ +package step.core.reporting; + +import ch.exense.commons.app.Configuration; +import org.bson.types.ObjectId; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.Mockito; +import step.core.GlobalContext; +import step.core.accessors.AbstractOrganizableObject; +import step.core.collections.inmemory.InMemoryCollectionFactory; +import step.core.deployment.WebApplicationConfigurationManager; +import step.core.entities.EntityManager; +import step.core.reporting.model.ReportLayout; +import step.framework.server.ServiceRegistrationCallback; +import step.framework.server.tables.TableRegistry; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +import static org.junit.Assert.*; + +public class ReportLayoutPluginTest { + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private GlobalContext context; + private ReportLayoutPlugin plugin; + private ReportLayoutAccessor accessor; + + @Before + public void setUp() throws Exception { + context = new GlobalContext(); + context.setCollectionFactory(new InMemoryCollectionFactory(new Properties())); + context.put(TableRegistry.class, new TableRegistry()); + context.setEntityManager(new EntityManager()); + context.setServiceRegistrationCallback(Mockito.mock(ServiceRegistrationCallback.class)); + context.setConfiguration(new Configuration()); + context.put(WebApplicationConfigurationManager.class, new WebApplicationConfigurationManager()); + + plugin = new ReportLayoutPlugin(); + plugin.serverStart(context); + accessor = context.require(ReportLayoutAccessor.class); + } + + private void setPresetsFolder(File folder) { + context.getConfiguration().putProperty( + ReportLayoutPlugin.PRESET_FOLDER_PATH_CONFIG_KEY, folder.getAbsolutePath()); + } + + /** Writes a preset JSON file with a fresh ObjectId. Returns the generated id. */ + private String writePresetFile(File folder, String fileName, String name) throws IOException { + String id = new ObjectId().toHexString(); + writePresetFileWithId(folder, fileName, name, id, "{\"type\":\"test\"}"); + return id; + } + + private void writePresetFileWithId(File folder, String fileName, String name, String id, String layoutJson) throws IOException { + File file = new File(folder, fileName); + try (FileWriter writer = new FileWriter(file)) { + writer.write("{\"id\":\"" + id + "\",\"name\":\"" + name + "\",\"layout\":" + layoutJson + "}"); + } + } + + private List getAllPresets() { + return accessor.getCollectionDriver() + .find(step.core.collections.Filters.equals( + ReportLayout.FIELD_VISIBILITY, + ReportLayout.ReportLayoutVisibility.Preset.name()), + null, null, null, 0) + .collect(Collectors.toList()); + } + + // --- initializeData with valid folder --- + + @Test + public void initializeData_singleValidFile_createsPreset() throws Exception { + File folder = tempFolder.newFolder("presets"); + writePresetFile(folder, "layout1.json", "My Preset"); + setPresetsFolder(folder); + + plugin.initializeData(context); + + List presets = getAllPresets(); + assertEquals(1, presets.size()); + assertEquals("My Preset", presets.get(0).getAttribute(AbstractOrganizableObject.NAME)); + assertEquals(ReportLayout.ReportLayoutVisibility.Preset, presets.get(0).visibility); + } + + @Test + public void initializeData_multipleValidFiles_createsAllPresets() throws Exception { + File folder = tempFolder.newFolder("presets"); + writePresetFile(folder, "alpha.json", "Alpha"); + writePresetFile(folder, "beta.json", "Beta"); + writePresetFile(folder, "gamma.json", "Gamma"); + setPresetsFolder(folder); + + plugin.initializeData(context); + + List presets = getAllPresets(); + assertEquals(3, presets.size()); + List names = presets.stream() + .map(p -> p.getAttribute(AbstractOrganizableObject.NAME)) + .collect(Collectors.toList()); + assertTrue(names.contains("Alpha")); + assertTrue(names.contains("Beta")); + assertTrue(names.contains("Gamma")); + } + + @Test + public void initializeData_preservesIdFromJsonFile() throws Exception { + File folder = tempFolder.newFolder("presets"); + String fixedId = new ObjectId().toHexString(); + writePresetFileWithId(folder, "layout.json", "Fixed Id Preset", fixedId, "{\"type\":\"test\"}"); + setPresetsFolder(folder); + + plugin.initializeData(context); + + List presets = getAllPresets(); + assertEquals(1, presets.size()); + assertEquals(fixedId, presets.get(0).getId().toHexString()); + } + + @Test + public void initializeData_nonJsonFilesIgnored() throws Exception { + File folder = tempFolder.newFolder("presets"); + writePresetFile(folder, "layout.json", "Valid Preset"); + new File(folder, "readme.txt").createNewFile(); + new File(folder, "layout.xml").createNewFile(); + setPresetsFolder(folder); + + plugin.initializeData(context); + + List presets = getAllPresets(); + assertEquals(1, presets.size()); + assertEquals("Valid Preset", presets.get(0).getAttribute(AbstractOrganizableObject.NAME)); + } + + // --- initializeData drops and recreates presets on each startup --- + + @Test + public void initializeData_dropsExistingPresetsBeforeLoading() throws Exception { + File folder = tempFolder.newFolder("presets"); + writePresetFile(folder, "initial.json", "Initial Preset"); + setPresetsFolder(folder); + plugin.initializeData(context); + assertEquals(1, getAllPresets().size()); + + // Replace file content and re-run initializeData (simulates restart) + new File(folder, "initial.json").delete(); + writePresetFile(folder, "updated.json", "Updated Preset"); + + plugin.initializeData(context); + + List presets = getAllPresets(); + assertEquals(1, presets.size()); + assertEquals("Updated Preset", presets.get(0).getAttribute(AbstractOrganizableObject.NAME)); + } + + @Test + public void initializeData_idempotent_sameIdEachRestart() throws Exception { + File folder = tempFolder.newFolder("presets"); + String fixedId = new ObjectId().toHexString(); + writePresetFileWithId(folder, "layout.json", "Stable Preset", fixedId, "{\"type\":\"test\"}"); + setPresetsFolder(folder); + + plugin.initializeData(context); + plugin.initializeData(context); + + List presets = getAllPresets(); + assertEquals("Second run should not duplicate the preset", 1, presets.size()); + assertEquals(fixedId, presets.get(0).getId().toHexString()); + } + + @Test + public void initializeData_emptyFolder_dropsAllExistingPresets() throws Exception { + ReportLayout existing = new ReportLayout(null, ReportLayout.ReportLayoutVisibility.Preset); + existing.addAttribute(AbstractOrganizableObject.NAME, "Old Preset"); + accessor.save(existing); + assertEquals(1, getAllPresets().size()); + + File emptyFolder = tempFolder.newFolder("empty-presets"); + setPresetsFolder(emptyFolder); + + plugin.initializeData(context); + + assertTrue("All presets should be dropped when folder is empty", getAllPresets().isEmpty()); + } + + @Test + public void initializeData_doesNotDropNonPresetLayouts() throws Exception { + ReportLayout privateLayout = new ReportLayout(null, ReportLayout.ReportLayoutVisibility.Private); + privateLayout.addAttribute(AbstractOrganizableObject.NAME, "My Private"); + privateLayout.setCreationUser("alice"); + accessor.save(privateLayout); + + ReportLayout sharedLayout = new ReportLayout(null, ReportLayout.ReportLayoutVisibility.Shared); + sharedLayout.addAttribute(AbstractOrganizableObject.NAME, "My Shared"); + sharedLayout.setCreationUser("bob"); + accessor.save(sharedLayout); + + File folder = tempFolder.newFolder("presets"); + writePresetFile(folder, "preset.json", "New Preset"); + setPresetsFolder(folder); + + plugin.initializeData(context); + + assertNotNull(accessor.get(privateLayout.getId())); + assertNotNull(accessor.get(sharedLayout.getId())); + assertEquals(1, getAllPresets().size()); + } + + // --- initializeData with missing / invalid folder --- + + @Test + public void initializeData_missingFolder_createsNoPresets() throws Exception { + context.getConfiguration().putProperty( + ReportLayoutPlugin.PRESET_FOLDER_PATH_CONFIG_KEY, + "/nonexistent/path/to/presets"); + + plugin.initializeData(context); + + assertTrue("No presets should be created when folder does not exist", getAllPresets().isEmpty()); + } + + @Test + public void initializeData_missingFolder_dropsExistingPresets() throws Exception { + ReportLayout existing = new ReportLayout(null, ReportLayout.ReportLayoutVisibility.Preset); + existing.addAttribute(AbstractOrganizableObject.NAME, "Existing Preset"); + accessor.save(existing); + + context.getConfiguration().putProperty( + ReportLayoutPlugin.PRESET_FOLDER_PATH_CONFIG_KEY, + "/nonexistent/path/to/presets"); + + plugin.initializeData(context); + + // The drop step runs unconditionally before the folder existence check + assertTrue("Existing presets are dropped even when folder is missing", getAllPresets().isEmpty()); + } + + // --- id validation --- + + @Test + public void initializeData_fileWithMissingId_isSkipped() throws Exception { + File folder = tempFolder.newFolder("presets"); + try (FileWriter writer = new FileWriter(new File(folder, "no-id.json"))) { + writer.write("{\"name\":\"No Id Preset\",\"layout\":{\"type\":\"test\"}}"); + } + setPresetsFolder(folder); + + plugin.initializeData(context); + + assertTrue("Preset with missing id should be skipped", getAllPresets().isEmpty()); + } + + @Test + public void initializeData_fileWithInvalidId_isSkipped() throws Exception { + File folder = tempFolder.newFolder("presets"); + try (FileWriter writer = new FileWriter(new File(folder, "bad-id.json"))) { + writer.write("{\"id\":\"not-a-valid-objectid\",\"name\":\"Bad Id Preset\",\"layout\":{\"type\":\"test\"}}"); + } + setPresetsFolder(folder); + + plugin.initializeData(context); + + assertTrue("Preset with invalid id should be skipped", getAllPresets().isEmpty()); + } + + @Test + public void initializeData_invalidJsonFile_skipsItAndLoadsOthers() throws Exception { + File folder = tempFolder.newFolder("presets"); + try (FileWriter writer = new FileWriter(new File(folder, "broken.json"))) { + writer.write("{ this is not valid json }"); + } + writePresetFile(folder, "valid.json", "Valid Preset"); + setPresetsFolder(folder); + + plugin.initializeData(context); + + List presets = getAllPresets(); + assertEquals("Valid preset should still be loaded despite broken file", 1, presets.size()); + assertEquals("Valid Preset", presets.get(0).getAttribute(AbstractOrganizableObject.NAME)); + } + + @Test + public void initializeData_allInvalidFiles_createsNoPresets() throws Exception { + File folder = tempFolder.newFolder("presets"); + try (FileWriter writer = new FileWriter(new File(folder, "broken1.json"))) { + writer.write("not json at all"); + } + try (FileWriter writer = new FileWriter(new File(folder, "no-id.json"))) { + writer.write("{\"name\":\"Missing Id\",\"layout\":{}}"); + } + setPresetsFolder(folder); + + plugin.initializeData(context); + + assertTrue(getAllPresets().isEmpty()); + } + + // --- preset layout content --- + + @Test + public void initializeData_presetLayoutHasCorrectContent() throws Exception { + File folder = tempFolder.newFolder("presets"); + String id = new ObjectId().toHexString(); + writePresetFileWithId(folder, "layout.json", "Dashboard", id, "{\"columns\":3,\"rows\":2}"); + setPresetsFolder(folder); + + plugin.initializeData(context); + + List presets = getAllPresets(); + assertEquals(1, presets.size()); + ReportLayout preset = presets.get(0); + assertEquals("Dashboard", preset.getAttribute(AbstractOrganizableObject.NAME)); + assertEquals(ReportLayout.ReportLayoutVisibility.Preset, preset.visibility); + assertEquals(id, preset.getId().toHexString()); + assertNotNull("Layout JSON should be parsed and stored", preset.layout); + } +} From 125297c936635c540c0f056160c06b8a42780b57 Mon Sep 17 00:00:00 2001 From: David Stephan Date: Thu, 12 Mar 2026 14:54:09 +0100 Subject: [PATCH 8/9] SED-4417 fixing test layout id --- .../reporting/layouts/presets/StepMainReportLayout.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json b/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json index f678c56ef..c654826d9 100644 --- a/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json +++ b/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json @@ -1,5 +1,5 @@ { - "uid": "fe687d84-5414-4fa0-ba2e-3bd09fef11b3", + "id": "69b010aeec94534eb48176db", "name": "Step Main Layout", "layout": { "widgets": [ From a6acf75e0b8d0994a613d0bb44193f47d0824345 Mon Sep 17 00:00:00 2001 From: David Stephan Date: Thu, 12 Mar 2026 17:05:42 +0100 Subject: [PATCH 9/9] SED-4417 PR feedbacks --- .../layouts/presets/StepMainReportLayout.json | 11 ----------- .../step/core/reporting/ReportLayoutServices.java | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json b/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json index c654826d9..c4ed57d7b 100644 --- a/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json +++ b/step-controller/step-controller-backend/src/test/resources/reporting/layouts/presets/StepMainReportLayout.json @@ -4,7 +4,6 @@ "layout": { "widgets": [ { - "id": "c4460688-41d9-4765-a260-d3bed46c4d50", "widgetType": "errorsWidget", "position": { "row": 1, @@ -14,7 +13,6 @@ } }, { - "id": "c11a02b1-de67-408a-9abe-f036f3ba7894", "widgetType": "testCases", "position": { "row": 2, @@ -24,7 +22,6 @@ } }, { - "id": "eb2af1f3-1037-409d-943f-35434a2cb0ed", "widgetType": "testCasesSummary", "position": { "row": 2, @@ -34,7 +31,6 @@ } }, { - "id": "8556aa3f-87fb-4300-b1e4-c7da0f7bbc9c", "widgetType": "keywordsSummary", "position": { "row": 5, @@ -44,7 +40,6 @@ } }, { - "id": "505e9f33-c342-40de-b390-01812dd6a2f7", "widgetType": "executionTree", "position": { "row": 5, @@ -54,7 +49,6 @@ } }, { - "id": "a1928775-e4de-42e1-aab6-74e357462c47", "widgetType": "keywordsList", "position": { "row": 8, @@ -64,7 +58,6 @@ } }, { - "id": "4cd5b4d3-f12e-43e2-bb91-7609d89098c7", "widgetType": "performanceOverview", "position": { "row": 8, @@ -74,7 +67,6 @@ } }, { - "id": "ef3f70e8-eed9-433d-ba07-c6c830eb1122", "widgetType": "errors", "position": { "row": 11, @@ -84,7 +76,6 @@ } }, { - "id": "d0d7bfd9-1d2e-4170-9fa1-a62000073d9f", "widgetType": "notificationSubscriptionForExecution", "position": { "row": 14, @@ -94,7 +85,6 @@ } }, { - "id": "7da124f6-89ba-42b6-a888-1502a784641f", "widgetType": "housekeepingSettingsForExecution", "position": { "row": 14, @@ -104,7 +94,6 @@ } }, { - "id": "11af33b5-48b9-46ba-9f9f-d9feb7ef2756", "widgetType": "currentOperations", "position": { "row": 17, diff --git a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java index cc71fce42..461bf72f2 100644 --- a/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java +++ b/step-controller/step-controller-server/src/main/java/step/core/reporting/ReportLayoutServices.java @@ -62,6 +62,7 @@ public ReportLayout get(String id) { @Secured(right = "{entity}-read") public ReportLayoutJson exportLayout(@PathParam("id") String id) { ReportLayout reportLayout = getEntity(id); + checkLayoutRight(reportLayout, READ_RIGHT); return new ReportLayoutJson(reportLayout); }