From 83c19f24020ae02615888f901761e361d2502a63 Mon Sep 17 00:00:00 2001 From: Marine LM Date: Mon, 16 Feb 2026 09:29:31 +0100 Subject: [PATCH 1/5] [frontend/backend] fix: add pagination on list widget Signed-off-by: Marine LM --- .../CustomDashboardService.java | 23 ++- .../openaev/rest/dashboard/DashboardApi.java | 18 +- .../rest/dashboard/DashboardService.java | 31 ++-- .../rest/exercise/ExerciseDashboardApi.java | 18 +- .../rest/scenario/ScenarioDashboardApi.java | 18 +- .../rest/settings/PlatformSettingsApi.java | 18 +- .../utils/es/EntitiesPaginationInput.java | 23 +++ .../es}/WidgetToEntitiesInput.java | 7 +- .../es}/WidgetToEntitiesOutput.java | 8 +- .../pagination/SearchPaginationInput.java | 24 +-- .../rest/dashboard/DashboardApiTest.java | 2 +- openaev-dev/docker-compose.yml | 3 +- .../actions/dashboards/dashboard-action.ts | 9 +- .../src/actions/exercises/exercise-action.ts | 9 +- .../src/actions/scenarios/scenario-actions.ts | 9 +- .../src/actions/settings/settings-action.ts | 9 +- .../scenario/analysis/ScenarioAnalysis.tsx | 4 +- .../analysis/SimulationAnalysis.tsx | 4 +- .../custom_dashboards/CustomDashboard.tsx | 4 +- .../CustomDashboardContext.tsx | 6 +- .../CustomDashboardWrapper.tsx | 6 +- .../widgetDataDrawer/WidgetDataDrawer.tsx | 63 ++++--- .../custom_dashboards/widgets/WidgetUtils.tsx | 7 +- .../custom_dashboards/widgets/WidgetViz.tsx | 24 ++- .../widgets/WidgetWrapper.tsx | 168 +++++++++--------- .../widgets/viz/list/ListWidget.tsx | 88 +++++---- .../queryable/pagination/PaginationHelpers.ts | 6 + .../pagination/usePaginationState.tsx | 56 ++++-- openaev-front/src/utils/api-types.d.ts | 88 +++++---- .../java/io/openaev/engine/EngineService.java | 5 +- .../io/openaev/engine/api/ListRuntime.java | 6 +- .../io/openaev/engine/query/EsEntities.java | 41 +++++ .../io/openaev/service/ElasticService.java | 22 ++- .../openaev/service/EsAttackPathService.java | 3 +- .../io/openaev/service/OpenSearchService.java | 22 ++- .../openaev/utils/pagination/Pagination.java | 29 +++ 36 files changed, 552 insertions(+), 329 deletions(-) create mode 100644 openaev-api/src/main/java/io/openaev/utils/es/EntitiesPaginationInput.java rename openaev-api/src/main/java/io/openaev/{rest/dashboard/model => utils/es}/WidgetToEntitiesInput.java (80%) rename openaev-api/src/main/java/io/openaev/{rest/dashboard/model => utils/es}/WidgetToEntitiesOutput.java (80%) create mode 100644 openaev-model/src/main/java/io/openaev/engine/query/EsEntities.java create mode 100644 openaev-model/src/main/java/io/openaev/utils/pagination/Pagination.java diff --git a/openaev-api/src/main/java/io/openaev/rest/custom_dashboard/CustomDashboardService.java b/openaev-api/src/main/java/io/openaev/rest/custom_dashboard/CustomDashboardService.java index 677bddba7c9..c5c47f488e1 100644 --- a/openaev-api/src/main/java/io/openaev/rest/custom_dashboard/CustomDashboardService.java +++ b/openaev-api/src/main/java/io/openaev/rest/custom_dashboard/CustomDashboardService.java @@ -11,18 +11,15 @@ import io.openaev.database.model.Widget; import io.openaev.database.raw.RawCustomDashboard; import io.openaev.database.repository.CustomDashboardRepository; -import io.openaev.engine.model.EsBase; -import io.openaev.engine.query.EsAttackPath; -import io.openaev.engine.query.EsAvgs; -import io.openaev.engine.query.EsCountInterval; -import io.openaev.engine.query.EsSeries; +import io.openaev.engine.query.*; import io.openaev.rest.custom_dashboard.form.CustomDashboardOutput; import io.openaev.rest.dashboard.DashboardService; -import io.openaev.rest.dashboard.model.WidgetToEntitiesInput; -import io.openaev.rest.dashboard.model.WidgetToEntitiesOutput; import io.openaev.rest.exception.BadRequestException; import io.openaev.service.PlatformSettingsService; import io.openaev.utils.FilterUtilsJpa; +import io.openaev.utils.es.EntitiesPaginationInput; +import io.openaev.utils.es.WidgetToEntitiesInput; +import io.openaev.utils.es.WidgetToEntitiesOutput; import io.openaev.utils.mapper.CustomDashboardMapper; import io.openaev.utils.pagination.SearchPaginationInput; import jakarta.persistence.EntityNotFoundException; @@ -290,15 +287,15 @@ public List dashboardSeriesOnResourceId( return this.dashboardService.series(widgetId, parameters); } - public List dashboardEntitiesOnResourceId( + public EsEntities dashboardEntitiesOnResourceId( @NotBlank final String resourceId, @NotBlank final String widgetId, - final Map parameters) { + final EntitiesPaginationInput input) { // verify that the widget is in the resource dashboard if (!isWidgetInResourceDashboard(resourceId, widgetId)) { throw new AccessDeniedException("Access denied"); } - return this.dashboardService.entities(widgetId, parameters); + return this.dashboardService.entities(widgetId, input.getParameters(), input.getPagination()); } public WidgetToEntitiesOutput widgetToEntitiesRuntimeOnResourceId( @@ -353,13 +350,13 @@ public List homeDashboardSeries( return dashboardService.series(widgetId, parameters); } - public List homeDashboardEntities( - @NotBlank final String widgetId, final Map parameters) { + public EsEntities homeDashboardEntities( + @NotBlank final String widgetId, final EntitiesPaginationInput input) { // verify that the widget is in the home dashboard if (!isWidgetInHomeDashboard(widgetId)) { throw new AccessDeniedException("Access denied"); } - return dashboardService.entities(widgetId, parameters); + return dashboardService.entities(widgetId, input.getParameters(), input.getPagination()); } public WidgetToEntitiesOutput homeWidgetToEntitiesRuntimeOnResourceId( diff --git a/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardApi.java b/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardApi.java index 8acf7034f13..daaf84522dd 100644 --- a/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardApi.java @@ -3,15 +3,12 @@ import io.openaev.aop.AccessControl; import io.openaev.database.model.Action; import io.openaev.database.model.ResourceType; -import io.openaev.engine.model.EsBase; import io.openaev.engine.model.EsSearch; -import io.openaev.engine.query.EsAttackPath; -import io.openaev.engine.query.EsAvgs; -import io.openaev.engine.query.EsCountInterval; -import io.openaev.engine.query.EsSeries; -import io.openaev.rest.dashboard.model.WidgetToEntitiesInput; -import io.openaev.rest.dashboard.model.WidgetToEntitiesOutput; +import io.openaev.engine.query.*; import io.openaev.rest.helper.RestBehavior; +import io.openaev.utils.es.EntitiesPaginationInput; +import io.openaev.utils.es.WidgetToEntitiesInput; +import io.openaev.utils.es.WidgetToEntitiesOutput; import jakarta.validation.Valid; import java.util.List; import java.util.Map; @@ -65,10 +62,9 @@ public List series( resourceId = "#widgetId", actionPerformed = Action.READ, resourceType = ResourceType.DASHBOARD) - public List entities( - @PathVariable final String widgetId, - @RequestBody(required = false) Map parameters) { - return this.dashboardService.entities(widgetId, parameters); + public EsEntities entities( + @PathVariable final String widgetId, @RequestBody EntitiesPaginationInput input) { + return this.dashboardService.entities(widgetId, input.getParameters(), input.getPagination()); } @PostMapping(DASHBOARD_URI + "/entities-runtime/{widgetId}") diff --git a/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardService.java b/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardService.java index 2cd3aea1752..c452cf5349b 100644 --- a/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardService.java +++ b/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardService.java @@ -8,18 +8,16 @@ import io.openaev.database.repository.UserRepository; import io.openaev.engine.EngineService; import io.openaev.engine.api.*; -import io.openaev.engine.model.EsBase; import io.openaev.engine.model.EsSearch; -import io.openaev.engine.query.EsAttackPath; -import io.openaev.engine.query.EsAvgs; -import io.openaev.engine.query.EsCountInterval; -import io.openaev.engine.query.EsSeries; +import io.openaev.engine.query.*; import io.openaev.rest.custom_dashboard.WidgetService; -import io.openaev.rest.dashboard.model.WidgetToEntitiesInput; -import io.openaev.rest.dashboard.model.WidgetToEntitiesOutput; import io.openaev.service.EsAttackPathService; import io.openaev.service.EsSecurityDomainService; +import io.openaev.utils.es.WidgetToEntitiesInput; +import io.openaev.utils.es.WidgetToEntitiesOutput; import io.openaev.utils.mapper.RawUserAuthMapper; +import io.openaev.utils.pagination.Pagination; +import jakarta.annotation.Nullable; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -102,11 +100,14 @@ public List series(String widgetId, Map parameters) { * * @param widgetContext the context containing widget, user, and parameter information * @param config the list configuration defining query parameters + * @param pagination pagination passed at runtime * @return a list of entities retrieved from the engine service */ - private List executeListQuery(WidgetContext widgetContext, ListConfiguration config) { + private EsEntities executeListQuery( + WidgetContext widgetContext, ListConfiguration config, Pagination pagination) { ListRuntime runtime = - new ListRuntime(config, widgetContext.parameters(), widgetContext.definitionParameters()); + new ListRuntime( + config, widgetContext.parameters(), widgetContext.definitionParameters(), pagination); return engineService.entities(widgetContext.user(), runtime); } @@ -115,12 +116,14 @@ private List executeListQuery(WidgetContext widgetContext, ListConfigura * * @param widgetId the id from the {@link Widget} with a list configuration * @param parameters parameters passed at runtime (e.g. filters) - * @return list of {@link EsBase} entities matching the list widget query + * @param pagination pagination passed at runtime + * @return list of entities matching the list widget query */ - public List entities(String widgetId, Map parameters) { + public EsEntities entities( + String widgetId, Map parameters, @Nullable Pagination pagination) { WidgetContext widgetContext = getWidgetContext(widgetId, parameters); ListConfiguration config = (ListConfiguration) widgetContext.widget().getWidgetConfiguration(); - return executeListQuery(widgetContext, config); + return executeListQuery(widgetContext, config, pagination); } /** @@ -145,7 +148,7 @@ public WidgetToEntitiesOutput widgetToEntitiesRuntime( String widgetId, WidgetToEntitiesInput input) { WidgetContext widgetContext = getWidgetContext(widgetId, input.getParameters()); ListConfiguration listConfig; - List datas; + EsEntities datas; if (isSecurityCoverageWidget(widgetContext.widget)) { listConfig = @@ -157,7 +160,7 @@ public WidgetToEntitiesOutput widgetToEntitiesRuntime( widgetContext.widget, input.getSeriesIndex(), input.getFilterValuesMap()); } - datas = executeListQuery(widgetContext, listConfig); + datas = executeListQuery(widgetContext, listConfig, input.getPagination()); return WidgetToEntitiesOutput.builder().listConfiguration(listConfig).esEntities(datas).build(); } diff --git a/openaev-api/src/main/java/io/openaev/rest/exercise/ExerciseDashboardApi.java b/openaev-api/src/main/java/io/openaev/rest/exercise/ExerciseDashboardApi.java index 61502a94351..771b9cf6222 100644 --- a/openaev-api/src/main/java/io/openaev/rest/exercise/ExerciseDashboardApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/exercise/ExerciseDashboardApi.java @@ -6,14 +6,11 @@ import io.openaev.database.model.Action; import io.openaev.database.model.CustomDashboard; import io.openaev.database.model.ResourceType; -import io.openaev.engine.model.EsBase; -import io.openaev.engine.query.EsAttackPath; -import io.openaev.engine.query.EsAvgs; -import io.openaev.engine.query.EsCountInterval; -import io.openaev.engine.query.EsSeries; +import io.openaev.engine.query.*; import io.openaev.rest.custom_dashboard.CustomDashboardService; -import io.openaev.rest.dashboard.model.WidgetToEntitiesInput; -import io.openaev.rest.dashboard.model.WidgetToEntitiesOutput; +import io.openaev.utils.es.EntitiesPaginationInput; +import io.openaev.utils.es.WidgetToEntitiesInput; +import io.openaev.utils.es.WidgetToEntitiesOutput; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -91,12 +88,11 @@ public List dashboardSeries( resourceId = "#simulationId", actionPerformed = Action.READ, resourceType = ResourceType.SIMULATION) - public List dashboardEntities( + public EsEntities dashboardEntities( @PathVariable final String simulationId, @PathVariable final String widgetId, - @RequestBody(required = false) Map parameters) { - return this.customDashboardService.dashboardEntitiesOnResourceId( - simulationId, widgetId, parameters); + @RequestBody EntitiesPaginationInput input) { + return this.customDashboardService.dashboardEntitiesOnResourceId(simulationId, widgetId, input); } @PostMapping(EXERCISE_URI + "/{simulationId}/dashboard/entities-runtime/{widgetId}") diff --git a/openaev-api/src/main/java/io/openaev/rest/scenario/ScenarioDashboardApi.java b/openaev-api/src/main/java/io/openaev/rest/scenario/ScenarioDashboardApi.java index 65f922b1369..550a7c4fab7 100644 --- a/openaev-api/src/main/java/io/openaev/rest/scenario/ScenarioDashboardApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/scenario/ScenarioDashboardApi.java @@ -6,14 +6,11 @@ import io.openaev.database.model.Action; import io.openaev.database.model.CustomDashboard; import io.openaev.database.model.ResourceType; -import io.openaev.engine.model.EsBase; -import io.openaev.engine.query.EsAttackPath; -import io.openaev.engine.query.EsAvgs; -import io.openaev.engine.query.EsCountInterval; -import io.openaev.engine.query.EsSeries; +import io.openaev.engine.query.*; import io.openaev.rest.custom_dashboard.CustomDashboardService; -import io.openaev.rest.dashboard.model.WidgetToEntitiesInput; -import io.openaev.rest.dashboard.model.WidgetToEntitiesOutput; +import io.openaev.utils.es.EntitiesPaginationInput; +import io.openaev.utils.es.WidgetToEntitiesInput; +import io.openaev.utils.es.WidgetToEntitiesOutput; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -90,12 +87,11 @@ public List dashboardSeries( resourceId = "#scenarioId", actionPerformed = Action.READ, resourceType = ResourceType.SCENARIO) - public List dashboardEntities( + public EsEntities dashboardEntities( @PathVariable final String scenarioId, @PathVariable final String widgetId, - @RequestBody(required = false) Map parameters) { - return this.customDashboardService.dashboardEntitiesOnResourceId( - scenarioId, widgetId, parameters); + @RequestBody EntitiesPaginationInput input) { + return this.customDashboardService.dashboardEntitiesOnResourceId(scenarioId, widgetId, input); } @PostMapping(SCENARIO_URI + "/{scenarioId}/dashboard/entities-runtime/{widgetId}") diff --git a/openaev-api/src/main/java/io/openaev/rest/settings/PlatformSettingsApi.java b/openaev-api/src/main/java/io/openaev/rest/settings/PlatformSettingsApi.java index 48bf5e5c658..7c67e464849 100644 --- a/openaev-api/src/main/java/io/openaev/rest/settings/PlatformSettingsApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/settings/PlatformSettingsApi.java @@ -5,20 +5,17 @@ import io.openaev.database.model.Action; import io.openaev.database.model.CustomDashboard; import io.openaev.database.model.ResourceType; -import io.openaev.engine.model.EsBase; -import io.openaev.engine.query.EsAttackPath; -import io.openaev.engine.query.EsAvgs; -import io.openaev.engine.query.EsCountInterval; -import io.openaev.engine.query.EsSeries; +import io.openaev.engine.query.*; import io.openaev.rest.custom_dashboard.CustomDashboardService; -import io.openaev.rest.dashboard.model.WidgetToEntitiesInput; -import io.openaev.rest.dashboard.model.WidgetToEntitiesOutput; import io.openaev.rest.helper.RestBehavior; import io.openaev.rest.settings.form.*; import io.openaev.rest.settings.response.CalderaSettings; import io.openaev.rest.settings.response.PlatformSettings; import io.openaev.service.CalderaSettingsService; import io.openaev.service.PlatformSettingsService; +import io.openaev.utils.es.EntitiesPaginationInput; +import io.openaev.utils.es.WidgetToEntitiesInput; +import io.openaev.utils.es.WidgetToEntitiesOutput; import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -167,10 +164,9 @@ public List homeDashboardSeries( @PostMapping("/home-dashboard/entities/{widgetId}") @AccessControl(actionPerformed = Action.READ, resourceType = ResourceType.PLATFORM_SETTING) - public List homeDashboardEntities( - @PathVariable final String widgetId, - @RequestBody(required = false) Map parameters) { - return customDashboardService.homeDashboardEntities(widgetId, parameters); + public EsEntities homeDashboardEntities( + @PathVariable final String widgetId, @RequestBody EntitiesPaginationInput input) { + return customDashboardService.homeDashboardEntities(widgetId, input); } @PostMapping("/home-dashboard/entities-runtime/{widgetId}") diff --git a/openaev-api/src/main/java/io/openaev/utils/es/EntitiesPaginationInput.java b/openaev-api/src/main/java/io/openaev/utils/es/EntitiesPaginationInput.java new file mode 100644 index 00000000000..d80aeddb154 --- /dev/null +++ b/openaev-api/src/main/java/io/openaev/utils/es/EntitiesPaginationInput.java @@ -0,0 +1,23 @@ +package io.openaev.utils.es; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openaev.utils.pagination.Pagination; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; +import java.util.Map; +import lombok.*; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Data +public class EntitiesPaginationInput { + @Schema(description = "Pagination to set (optional)", nullable = true) + @JsonProperty("pagination") + @Nullable + Pagination pagination; + + @Schema(description = "Parameters to set") + @JsonProperty("parameters") + Map parameters; +} diff --git a/openaev-api/src/main/java/io/openaev/rest/dashboard/model/WidgetToEntitiesInput.java b/openaev-api/src/main/java/io/openaev/utils/es/WidgetToEntitiesInput.java similarity index 80% rename from openaev-api/src/main/java/io/openaev/rest/dashboard/model/WidgetToEntitiesInput.java rename to openaev-api/src/main/java/io/openaev/utils/es/WidgetToEntitiesInput.java index 02a854d560a..f58f14b0518 100644 --- a/openaev-api/src/main/java/io/openaev/rest/dashboard/model/WidgetToEntitiesInput.java +++ b/openaev-api/src/main/java/io/openaev/utils/es/WidgetToEntitiesInput.java @@ -1,6 +1,7 @@ -package io.openaev.rest.dashboard.model; +package io.openaev.utils.es; import com.fasterxml.jackson.annotation.JsonProperty; +import io.openaev.utils.pagination.Pagination; import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; import java.util.Map; @@ -24,4 +25,8 @@ public class WidgetToEntitiesInput { @JsonProperty("parameters") @Schema(description = "Additional parameters for the widget") private Map parameters; + + @JsonProperty("pagination") + @Schema(description = "Pagination for the widget") + private Pagination pagination; } diff --git a/openaev-api/src/main/java/io/openaev/rest/dashboard/model/WidgetToEntitiesOutput.java b/openaev-api/src/main/java/io/openaev/utils/es/WidgetToEntitiesOutput.java similarity index 80% rename from openaev-api/src/main/java/io/openaev/rest/dashboard/model/WidgetToEntitiesOutput.java rename to openaev-api/src/main/java/io/openaev/utils/es/WidgetToEntitiesOutput.java index d518a9960f6..80d778eb0ce 100644 --- a/openaev-api/src/main/java/io/openaev/rest/dashboard/model/WidgetToEntitiesOutput.java +++ b/openaev-api/src/main/java/io/openaev/utils/es/WidgetToEntitiesOutput.java @@ -1,10 +1,9 @@ -package io.openaev.rest.dashboard.model; +package io.openaev.utils.es; import com.fasterxml.jackson.annotation.JsonProperty; import io.openaev.engine.api.ListConfiguration; -import io.openaev.engine.model.EsBase; +import io.openaev.engine.query.EsEntities; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.Setter; @@ -13,7 +12,6 @@ @Setter @Builder public class WidgetToEntitiesOutput { - @Schema( description = "List configuration generated based on the input widget id and filter value") @JsonProperty("list_configuration") @@ -21,5 +19,5 @@ public class WidgetToEntitiesOutput { @Schema(description = "List of entities") @JsonProperty("es_entities") - private List esEntities; + private EsEntities esEntities; } diff --git a/openaev-api/src/main/java/io/openaev/utils/pagination/SearchPaginationInput.java b/openaev-api/src/main/java/io/openaev/utils/pagination/SearchPaginationInput.java index a200e83e16f..ee5135aa184 100644 --- a/openaev-api/src/main/java/io/openaev/utils/pagination/SearchPaginationInput.java +++ b/openaev-api/src/main/java/io/openaev/utils/pagination/SearchPaginationInput.java @@ -2,31 +2,17 @@ import io.openaev.database.model.Filters.FilterGroup; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; import java.util.ArrayList; import java.util.List; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; +import lombok.experimental.SuperBuilder; @AllArgsConstructor @NoArgsConstructor -@Builder +@SuperBuilder @Data -public class SearchPaginationInput { - - @Schema(description = "Page number to get") - @NotNull - @Min(0) - int page = 0; - - @Schema(description = "Element number by page") - @NotNull - @Max(1000) - int size = 20; +@EqualsAndHashCode(callSuper = true) +public class SearchPaginationInput extends Pagination { @Schema(description = "Filter object to search within filterable attributes") private FilterGroup filterGroup; diff --git a/openaev-api/src/test/java/io/openaev/rest/dashboard/DashboardApiTest.java b/openaev-api/src/test/java/io/openaev/rest/dashboard/DashboardApiTest.java index f65320753c6..3b8eaac9c2b 100644 --- a/openaev-api/src/test/java/io/openaev/rest/dashboard/DashboardApiTest.java +++ b/openaev-api/src/test/java/io/openaev/rest/dashboard/DashboardApiTest.java @@ -21,8 +21,8 @@ import io.openaev.engine.api.HistogramInterval; import io.openaev.engine.api.ListConfiguration; import io.openaev.engine.api.SortDirection; -import io.openaev.rest.dashboard.model.WidgetToEntitiesInput; import io.openaev.utils.CustomDashboardTimeRange; +import io.openaev.utils.es.WidgetToEntitiesInput; import io.openaev.utils.fixtures.*; import io.openaev.utils.fixtures.composers.*; import io.openaev.utils.fixtures.files.AttackPatternFixture; diff --git a/openaev-dev/docker-compose.yml b/openaev-dev/docker-compose.yml index e14769a467c..a5cf829bdbb 100644 --- a/openaev-dev/docker-compose.yml +++ b/openaev-dev/docker-compose.yml @@ -244,14 +244,13 @@ services: xtm-composer: image: filigran/xtm-composer:latest platform: linux/amd64 - network_mode: host # allow connecting to non docker local instance environment: - MANAGER__ID=${XTM_COMPOSER_ID} - "MANAGER__NAME=XTM Integrations Manager" - MANAGER__CREDENTIALS_KEY_FILEPATH=/keys/private_key.pem - OPENAEV__ENABLE=true - OPENCTI__ENABLE=false - - OPENAEV__URL=http://localhost:8080 + - OPENAEV__URL=http://192.168.56.1:8080 - OPENAEV__TOKEN=${OPENAEV_ADMIN_TOKEN} - OPENAEV__DAEMON__SELECTOR=docker # Workaround: pending https://github.com/FiligranHQ/xtm-composer/issues/64, diff --git a/openaev-front/src/actions/dashboards/dashboard-action.ts b/openaev-front/src/actions/dashboards/dashboard-action.ts index 6937b565b64..ff20dd887f4 100644 --- a/openaev-front/src/actions/dashboards/dashboard-action.ts +++ b/openaev-front/src/actions/dashboards/dashboard-action.ts @@ -1,5 +1,5 @@ import { simplePostCall } from '../../utils/Action'; -import { type WidgetToEntitiesInput } from '../../utils/api-types'; +import { type Pagination, type WidgetToEntitiesInput } from '../../utils/api-types'; export const DASHBOARD_URI = '/api/dashboards'; @@ -15,8 +15,11 @@ export const series = (widgetId: string, parameters: Record) => { - return simplePostCall(`${DASHBOARD_URI}/entities/${widgetId}`, parameters); +export const entities = (widgetId: string, parameters: Record, pagination?: Pagination | null) => { + return simplePostCall(`${DASHBOARD_URI}/entities/${widgetId}`, { + parameters, + pagination, + }); }; export const widgetToEntitiesRuntime = (widgetId: string, input: WidgetToEntitiesInput) => { diff --git a/openaev-front/src/actions/exercises/exercise-action.ts b/openaev-front/src/actions/exercises/exercise-action.ts index 8c36426a2d3..e519713d125 100644 --- a/openaev-front/src/actions/exercises/exercise-action.ts +++ b/openaev-front/src/actions/exercises/exercise-action.ts @@ -11,7 +11,7 @@ import { type LessonsCategoryUpdateInput, type LessonsQuestionCreateInput, type LessonsQuestionUpdateInput, - type LessonsSendInput, + type LessonsSendInput, type Pagination, type SearchPaginationInput, type WidgetToEntitiesInput, } from '../../utils/api-types'; @@ -221,8 +221,11 @@ export const seriesBySimulation = (simulationId: string, widgetId: string, param return simplePostCall(`${EXERCISE_URI}/${simulationId}/dashboard/series/${widgetId}`, parameters); }; -export const entitiesBySimulation = (simulationId: string, widgetId: string, parameters: Record) => { - return simplePostCall(`${EXERCISE_URI}/${simulationId}/dashboard/entities/${widgetId}`, parameters); +export const entitiesBySimulation = (simulationId: string, widgetId: string, parameters: Record, pagination?: Pagination) => { + return simplePostCall(`${EXERCISE_URI}/${simulationId}/dashboard/entities/${widgetId}`, { + parameters, + pagination, + }); }; export const widgetToEntitiesBySimulation = (simulationId: string, widgetId: string, input: WidgetToEntitiesInput) => { diff --git a/openaev-front/src/actions/scenarios/scenario-actions.ts b/openaev-front/src/actions/scenarios/scenario-actions.ts index efcd0104b9d..24c6bded7ae 100644 --- a/openaev-front/src/actions/scenarios/scenario-actions.ts +++ b/openaev-front/src/actions/scenarios/scenario-actions.ts @@ -16,7 +16,7 @@ import { type LessonsCategoryUpdateInput, type LessonsInput, type LessonsQuestionCreateInput, - type LessonsQuestionUpdateInput, + type LessonsQuestionUpdateInput, type Pagination, type Scenario, type ScenarioInput, type ScenarioRecurrenceInput, @@ -282,8 +282,11 @@ export const seriesByScenario = (scenarioId: string, widgetId: string, parameter return simplePostCall(`/api/scenarios/${scenarioId}/dashboard/series/${widgetId}`, parameters); }; -export const entitiesByScenario = (scenarioId: string, widgetId: string, parameters: Record) => { - return simplePostCall(`/api/scenarios/${scenarioId}/dashboard/entities/${widgetId}`, parameters); +export const entitiesByScenario = (scenarioId: string, widgetId: string, parameters: Record, pagination?: Pagination) => { + return simplePostCall(`/api/scenarios/${scenarioId}/dashboard/entities/${widgetId}`, { + parameters, + pagination, + }); }; export const widgetToEntitiesByByScenario = (scenarioId: string, widgetId: string, input: WidgetToEntitiesInput) => { diff --git a/openaev-front/src/actions/settings/settings-action.ts b/openaev-front/src/actions/settings/settings-action.ts index 46e766deb02..f0721703b89 100644 --- a/openaev-front/src/actions/settings/settings-action.ts +++ b/openaev-front/src/actions/settings/settings-action.ts @@ -1,5 +1,5 @@ import { simpleCall, simplePostCall } from '../../utils/Action'; -import type { WidgetToEntitiesInput } from '../../utils/api-types'; +import type { Pagination, WidgetToEntitiesInput } from '../../utils/api-types'; export const SETTINGS_URI = '/api/settings'; export const fetchHomeDashboard = () => { @@ -18,8 +18,11 @@ export const homeDashboardSeries = (widgetId: string, parameters: Record) => { - return simplePostCall(`${SETTINGS_URI}/home-dashboard/entities/${widgetId}`, parameters); +export const homeDashboardEntities = (widgetId: string, parameters: Record, pagination?: Pagination) => { + return simplePostCall(`${SETTINGS_URI}/home-dashboard/entities/${widgetId}`, { + parameters, + pagination, + }); }; export const homeWidgetToEntitiesRuntime = (widgetId: string, input: WidgetToEntitiesInput) => { diff --git a/openaev-front/src/admin/components/scenarios/scenario/analysis/ScenarioAnalysis.tsx b/openaev-front/src/admin/components/scenarios/scenario/analysis/ScenarioAnalysis.tsx index 54f3b883e29..789e9768dbb 100644 --- a/openaev-front/src/admin/components/scenarios/scenario/analysis/ScenarioAnalysis.tsx +++ b/openaev-front/src/admin/components/scenarios/scenario/analysis/ScenarioAnalysis.tsx @@ -13,7 +13,7 @@ import type { ScenariosHelper } from '../../../../../actions/scenarios/scenario- import { SCENARIO_SIMULATIONS } from '../../../../../components/common/queryable/filter/constants'; import { useHelper } from '../../../../../store'; import { - type CustomDashboard, + type CustomDashboard, type Pagination, type Scenario, type SortField, type WidgetToEntitiesInput, @@ -104,7 +104,7 @@ const ScenarioAnalysis = () => { fetchCount: (widgetId: string, params: Record) => countByScenario(scenarioId, widgetId, params), fetchAverage: (widgetId: string, params: Record) => averageByScenario(scenarioId, widgetId, params), fetchSeries: (widgetId: string, params: Record) => seriesByScenario(scenarioId, widgetId, params), - fetchEntities: (widgetId: string, params: Record) => entitiesByScenario(scenarioId, widgetId, params), + fetchEntities: (widgetId: string, params: Record, pagination?: Pagination) => entitiesByScenario(scenarioId, widgetId, params, pagination), fetchEntitiesRuntime: (widgetId: string, input: WidgetToEntitiesInput) => widgetToEntitiesByByScenario(scenarioId, widgetId, input), fetchAttackPaths: (widgetId: string, params: Record) => attackPathsByScenario(scenarioId, widgetId, params), }; diff --git a/openaev-front/src/admin/components/simulations/simulation/analysis/SimulationAnalysis.tsx b/openaev-front/src/admin/components/simulations/simulation/analysis/SimulationAnalysis.tsx index af95647c99f..eac6c335b87 100644 --- a/openaev-front/src/admin/components/simulations/simulation/analysis/SimulationAnalysis.tsx +++ b/openaev-front/src/admin/components/simulations/simulation/analysis/SimulationAnalysis.tsx @@ -11,7 +11,7 @@ import type { ExercisesHelper } from '../../../../../actions/exercises/exercise- import { useHelper } from '../../../../../store'; import { type CustomDashboard, - type Exercise, + type Exercise, type Pagination, type WidgetToEntitiesInput, } from '../../../../../utils/api-types'; import { useAppDispatch } from '../../../../../utils/hooks'; @@ -78,7 +78,7 @@ const SimulationAnalysis = () => { fetchAverage: (widgetId: string, params: Record) => averageBySimulation(exerciseId, widgetId, params), fetchSeries: (widgetId: string, params: Record) => seriesBySimulation(exerciseId, widgetId, params), fetchEntitiesRuntime: (widgetId: string, input: WidgetToEntitiesInput) => widgetToEntitiesBySimulation(exerciseId, widgetId, input), - fetchEntities: (widgetId: string, params: Record) => entitiesBySimulation(exerciseId, widgetId, params), + fetchEntities: (widgetId: string, params: Record, pagination?: Pagination) => entitiesBySimulation(exerciseId, widgetId, params, pagination), fetchAttackPaths: (widgetId: string, params: Record) => attackPathsBySimulation(exerciseId, widgetId, params), }; diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboard.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboard.tsx index e60e8333593..6e282a12274 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboard.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboard.tsx @@ -12,7 +12,7 @@ import { widgetToEntitiesRuntime, } from '../../../../actions/dashboards/dashboard-action'; import { useFormatter } from '../../../../components/i18n'; -import type { CustomDashboard, WidgetToEntitiesInput } from '../../../../utils/api-types'; +import type { CustomDashboard, Pagination, WidgetToEntitiesInput } from '../../../../utils/api-types'; import { AbilityContext, Can } from '../../../../utils/permissions/PermissionsProvider'; import { ACTIONS, SUBJECTS } from '../../../../utils/permissions/types'; import CustomDashboardEditHeader from './CustomDashboardEditHeader'; @@ -31,7 +31,7 @@ const CustomDashboard = () => { fetchAverage: (widgetId: string, params: Record) => average(widgetId, params), fetchCount: (widgetId: string, params: Record) => count(widgetId, params), fetchSeries: (widgetId: string, params: Record) => series(widgetId, params), - fetchEntities: (widgetId: string, params: Record) => entities(widgetId, params), + fetchEntities: (widgetId: string, params: Record, pagination?: Pagination) => entities(widgetId, params, pagination), fetchEntitiesRuntime: (widgetId: string, input: WidgetToEntitiesInput) => widgetToEntitiesRuntime(widgetId, input), fetchAttackPaths: (widgetId: string, params: Record) => attackPaths(widgetId, params), }), [customDashboardId]); diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboardContext.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboardContext.tsx index 7706c6a5127..5876aae5e6a 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboardContext.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboardContext.tsx @@ -12,8 +12,8 @@ import { type SearchOptionsConfig } from '../../../../components/common/queryabl import { type CustomDashboard, type EsAttackPath, type EsAvgs, - type EsBase, type EsCountInterval, - type EsSeries, + type EsCountInterval, type EsEntities, + type EsSeries, type Pagination, type WidgetToEntitiesInput, type WidgetToEntitiesOutput, } from '../../../../utils/api-types'; import { type WidgetDataDrawerConf } from './widgetDataDrawer/WidgetDataDrawer'; @@ -32,7 +32,7 @@ export interface CustomDashboardContextType { fetchAverage: (widgetId: string, params: Record) => Promise>; fetchCount: (widgetId: string, params: Record) => Promise>; fetchSeries: (widgetId: string, params: Record) => Promise>; - fetchEntities: (widgetId: string, params: Record) => Promise>; + fetchEntities: (widgetId: string, params: Record, pagination?: Pagination) => Promise>; fetchEntitiesRuntime: (widgetId: string, input: WidgetToEntitiesInput) => Promise>; fetchAttackPaths: (widgetId: string, params: Record) => Promise>; contextId?: string; diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboardWrapper.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboardWrapper.tsx index cd6e680a0fa..38cb3c1ef5e 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboardWrapper.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/CustomDashboardWrapper.tsx @@ -7,8 +7,8 @@ import Loader from '../../../../components/Loader'; import type { CustomDashboard, EsAttackPath, EsAvgs, - EsBase, EsCountInterval, - EsSeries, + EsCountInterval, EsEntities, + EsSeries, Pagination, WidgetToEntitiesInput, WidgetToEntitiesOutput, } from '../../../../utils/api-types'; @@ -30,7 +30,7 @@ interface CustomDashboardConfiguration { fetchCount: (widgetId: string, params: Record) => Promise>; fetchAverage: (widgetId: string, params: Record) => Promise>; fetchSeries: (widgetId: string, params: Record) => Promise>; - fetchEntities: (widgetId: string, params: Record) => Promise>; + fetchEntities: (widgetId: string, params: Record, pagination?: Pagination) => Promise>; fetchEntitiesRuntime: (widgetId: string, input: WidgetToEntitiesInput) => Promise>; fetchAttackPaths: (widgetId: string, params: Record) => Promise>; } diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgetDataDrawer/WidgetDataDrawer.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgetDataDrawer/WidgetDataDrawer.tsx index 984be32aad8..896fa6f1069 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgetDataDrawer/WidgetDataDrawer.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgetDataDrawer/WidgetDataDrawer.tsx @@ -1,13 +1,13 @@ import { Typography } from '@mui/material'; -import { useContext, useEffect, useMemo, useState } from 'react'; +import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { useSearchParams } from 'react-router'; import Drawer from '../../../../../components/common/Drawer'; import { useFormatter } from '../../../../../components/i18n'; import Loader from '../../../../../components/Loader'; import { - type EsBase, - type ListConfiguration, + type EsEntities, + type ListConfiguration, type Pagination, type WidgetToEntitiesInput, } from '../../../../../utils/api-types'; import { CustomDashboardContext } from '../CustomDashboardContext'; @@ -32,35 +32,47 @@ const WidgetDataDrawer = () => { }, [searchParams]); const [open, setOpen] = useState(false); - const [listDatas, setListDatas] = useState([]); + const [paginatedEntities, setPaginatedEntities] = useState(); const [listConfig, setListConfig] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - if (!customDashboard || !widgetId || filterValues == null || !seriesIndex) { - setOpen(false); - return; - } - setLoading(true); - setOpen(true); + const [initialLoading, setInitialLoading] = useState(true); // full widget loader + const [contentLoading, setContentLoading] = useState(false); + const fetchEntitiesAtRuntime = useCallback(async (currentWidgetId: string, pagination?: Pagination) => { const params: Record = Object.fromEntries( Object.entries(customDashboardParameters).map(([key, val]) => [key, val.value]), ); - fetchEntitiesRuntime(widgetId, { + return fetchEntitiesRuntime(currentWidgetId, { filter_values_map: filterValues, series_index: Number(seriesIndex), parameters: params, + pagination, }).then(({ data }) => { - setListDatas(data.es_entities ?? []); + setPaginatedEntities(data.es_entities); setListConfig(data.list_configuration); - setLoading(false); }).catch(() => { setListConfig(null); - setLoading(false); }); + }, [widgetId, filterValues]); + + useEffect(() => { + if (!customDashboard || !widgetId || filterValues == null || !seriesIndex) { + setOpen(false); + return; + } + setInitialLoading(true); + setOpen(true); + fetchEntitiesAtRuntime(widgetId).then(() => setInitialLoading(false)); }, [widgetId, filterValues, seriesIndex]); + const onPaginationChange = (pagination: Pagination) => { + if (!widgetId) { + setOpen(false); + return; + } + setContentLoading(true); + fetchEntitiesAtRuntime(widgetId, pagination).then(() => setContentLoading(false)); + }; + return ( { title={t('Display list')} > <> - {loading && } - {(!loading && listConfig == null) && {t('No data to display')}} - {(!loading && listConfig != null) && } + {initialLoading && } + {(!initialLoading && listConfig == null) && {t('No data to display')}} + {(!initialLoading && listConfig != null && paginatedEntities != null) + && ( + + )} ); diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetUtils.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetUtils.tsx index f3625aa240e..d5413918358 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetUtils.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetUtils.tsx @@ -4,8 +4,8 @@ import { AlignHorizontalLeft, ChartBar, ChartDonut, ChartLine, Counter } from 'm import { type CustomDashboardParameters, type EsAttackPath, - type EsAvgs, type EsBase, - type EsCountInterval, + type EsAvgs, + type EsCountInterval, type EsEntities, type EsSeries, type Exercise, type Filter, @@ -71,6 +71,7 @@ export const widgetVisualizationTypes: { { category: 'list', seriesLimit: 1, + limit: false, }, { category: 'number', @@ -286,7 +287,7 @@ export type WidgetVizData } | { type: WidgetVizDataType.ENTITIES; - data: EsBase[]; + data: EsEntities; } | { type: WidgetVizDataType.ATTACK_PATHS; diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetViz.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetViz.tsx index 97b4645e691..4b85c0a1482 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetViz.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetViz.tsx @@ -1,7 +1,13 @@ import { memo } from 'react'; import { useFormatter } from '../../../../../components/i18n'; -import { type EsSeries, type ListConfiguration, type StructuralHistogramWidget, type Widget } from '../../../../../utils/api-types'; +import { + type EsSeries, + type ListConfiguration, + type Pagination, + type StructuralHistogramWidget, + type Widget, +} from '../../../../../utils/api-types'; import AttackPathContextLayer from './viz/attack_paths/AttackPathContextLayer'; import SecurityDomainsWidget from './viz/domains/SecurityDomainsWidget'; import type { EsAvgsExtended } from './viz/domains/SecurityDomainsWidgetUtils'; @@ -20,6 +26,8 @@ interface WidgetTemporalVizProps { setFullscreen: (fullscreen: boolean) => void; errorMessage: string; vizData: WidgetVizData; + onPaginationChange: (paginationInput: Pagination) => void; + contentLoading?: boolean; } export type SerieData = { @@ -47,7 +55,7 @@ const computeSeriesData = (esSeries: EsSeries[]) => { }); }; -const WidgetViz = ({ widget, fullscreen, setFullscreen, vizData, errorMessage }: WidgetTemporalVizProps) => { +const WidgetViz = ({ widget, fullscreen, setFullscreen, vizData, errorMessage, onPaginationChange, contentLoading = false }: WidgetTemporalVizProps) => { const { t } = useFormatter(); const seriesData = vizData.type === WidgetVizDataType.SERIES @@ -128,7 +136,17 @@ const WidgetViz = ({ widget, fullscreen, setFullscreen, vizData, errorMessage }: if (vizData.type !== WidgetVizDataType.ENTITIES) { return 'Not implemented yet'; } - return (); + return ( + + ); case 'number': if (vizData.type !== WidgetVizDataType.NUMBER) { return 'Not implemented yet'; diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetWrapper.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetWrapper.tsx index 0e2360013b0..232a10d1bd6 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetWrapper.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/WidgetWrapper.tsx @@ -1,14 +1,14 @@ import { useTheme } from '@mui/material/styles'; -import { type SyntheticEvent, useContext, useEffect, useRef, useState } from 'react'; +import { type SyntheticEvent, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import usePaginationState from '../../../../../components/common/queryable/pagination/usePaginationState'; import { ErrorBoundary } from '../../../../../components/Error'; import Loader from '../../../../../components/Loader'; import { type EsAttackPath, type EsAvgs, - type EsBase, - type EsCountInterval, - type EsSeries, + type EsCountInterval, type EsEntities, + type EsSeries, type Pagination, type Widget, } from '../../../../../utils/api-types'; import { CustomDashboardContext, type ParameterOption } from '../CustomDashboardContext'; @@ -34,6 +34,13 @@ const buildParams = (parameters: Record): Record, pagination?: Pagination) => Promise<{ data: WidgetDataResponse }>; + transformData?: (data: WidgetDataResponse) => unknown; +}; + const WidgetWrapper = ({ widget, fullscreen, @@ -45,9 +52,43 @@ const WidgetWrapper = ({ }: WidgetWrapperProps) => { const theme = useTheme(); const [vizData, setVizData] = useState({ type: WidgetVizDataType.NONE }); - const [loading, setLoading] = useState(true); + const [initialLoading, setInitialLoading] = useState(true); // full widget loader + const [contentLoading, setContentLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); const { customDashboardParameters, fetchCount, fetchSeries, fetchEntities, fetchAttackPaths, fetchAverage } = useContext(CustomDashboardContext); + const { elementsPerPage, page, handleChangePagination } = widget.widget_type === 'list' + ? usePaginationState(100, undefined, `widget-list-${widget.widget_id}`) + : { + elementsPerPage: 0, + page: 0, + handleChangePagination: () => {}, + }; + + const WIDGET_CONFIG: Record = { + 'attack-path': { + vizType: WidgetVizDataType.ATTACK_PATHS, + fetchFn: fetchAttackPaths, + }, + 'number': { + vizType: WidgetVizDataType.NUMBER, + fetchFn: fetchCount, + }, + 'average': { + vizType: WidgetVizDataType.AVERAGE, + fetchFn: fetchAverage, + transformData: data => determinePercentage(data as EsAvgs, theme), + }, + 'list': { + vizType: WidgetVizDataType.ENTITIES, + fetchFn: fetchEntities, + }, + }; + + const DEFAULT_CONFIG: WidgetFetchConfig = { + vizType: WidgetVizDataType.SERIES, + fetchFn: fetchSeries, + }; // Use ref to track if component is mounted const isMountedRef = useRef(true); @@ -58,93 +99,54 @@ const WidgetWrapper = ({ }; }, []); - useEffect(() => { - setLoading(true); - setErrorMessage(''); - - const params = buildParams(customDashboardParameters); - type WidgetDataResponse = EsAttackPath[] | EsCountInterval | EsAvgs | EsBase[] | EsSeries[]; - let fetchFunction: (id: string, p: Record) => Promise<{ data: WidgetDataResponse }>; - let vizType: WidgetVizDataType; - - switch (widget.widget_type) { - case 'attack-path': - fetchFunction = fetchAttackPaths; - vizType = WidgetVizDataType.ATTACK_PATHS; - break; - case 'number': - fetchFunction = fetchCount; - vizType = WidgetVizDataType.NUMBER; - break; - case 'average': { - fetchFunction = fetchAverage; - vizType = WidgetVizDataType.AVERAGE; - break; - } - case 'list': - fetchFunction = fetchEntities; - vizType = WidgetVizDataType.ENTITIES; - break; - default: - fetchFunction = fetchSeries; - vizType = WidgetVizDataType.SERIES; - } - - fetchFunction(widget.widget_id, params) - .then((response) => { - if (!isMountedRef.current) return; + const fetchWidgetData = useCallback( + async (pagination: Pagination) => { + setErrorMessage(''); + + const params = buildParams(customDashboardParameters); + const config = WIDGET_CONFIG[widget.widget_type] ?? DEFAULT_CONFIG; + + config.fetchFn(widget.widget_id, params, pagination).then((response) => { if (response.data) { - switch (vizType) { - case WidgetVizDataType.SERIES: - setVizData({ - type: WidgetVizDataType.SERIES, - data: response.data as EsSeries[], - }); - break; - case WidgetVizDataType.ENTITIES: - setVizData({ - type: WidgetVizDataType.ENTITIES, - data: response.data as EsBase[], - }); - break; - case WidgetVizDataType.ATTACK_PATHS: - setVizData({ - type: WidgetVizDataType.ATTACK_PATHS, - data: response.data as EsAttackPath[], - }); - break; - case WidgetVizDataType.NUMBER: - setVizData({ - type: WidgetVizDataType.NUMBER, - data: response.data as EsCountInterval, - }); - break; - case WidgetVizDataType.AVERAGE: - setVizData({ - type: WidgetVizDataType.AVERAGE, - data: determinePercentage(response.data as EsAvgs, theme), - }); - break; - default: break; - } + setVizData({ + type: config.vizType, + data: config.transformData + ? config.transformData(response.data) + : response.data, + } as WidgetVizData); } - }) - .catch((error) => { + }).catch((error) => { if (!isMountedRef.current) return; setErrorMessage(error.message); - }) - .finally(() => { - if (isMountedRef.current) { - setLoading(false); - } }); - }, [widget.widget_id, widget.widget_type, widget.widget_config, customDashboardParameters, fetchAttackPaths, fetchCount, fetchEntities, fetchSeries]); + }, + [widget.widget_id, widget.widget_type, widget.widget_config, customDashboardParameters], + ); + + useEffect(() => { + if (!isMountedRef.current) return; + setInitialLoading(true); + fetchWidgetData({ + page, + size: elementsPerPage, + }).then(() => { + if (isMountedRef.current) { + setInitialLoading(false); + } + }); + }, [fetchWidgetData]); const handleMouseDown = (e: SyntheticEvent) => e.stopPropagation(); const handleTouchStart = (e: SyntheticEvent) => e.stopPropagation(); const isResizing = widget.widget_id === idToResize; + const onPaginationChange = (pagination: Pagination) => { + setContentLoading(true); + handleChangePagination(pagination); + fetchWidgetData(pagination).then(() => setContentLoading(false)); + }; + return (
- {loading ? ( + {initialLoading ? ( ) : ( )}
diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/viz/list/ListWidget.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/viz/list/ListWidget.tsx index 01442196a67..d7b16493a74 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/viz/list/ListWidget.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/viz/list/ListWidget.tsx @@ -5,21 +5,23 @@ import { ListItem as MuiListItem, ListItemButton, ListItemIcon, - ListItemText, + ListItemText, TablePagination, } from '@mui/material'; -import { memo, useCallback, useMemo } from 'react'; +import { type ChangeEvent, memo, useCallback, useMemo } from 'react'; import { useNavigate } from 'react-router'; import { makeStyles } from 'tss-react/mui'; import { type AttackPatternHelper } from '../../../../../../../actions/attack_patterns/attackpattern-helper'; -import { initSorting } from '../../../../../../../components/common/queryable/Page'; -import { buildSearchPagination } from '../../../../../../../components/common/queryable/QueryableUtils'; -import SortHeadersComponentV2 from '../../../../../../../components/common/queryable/sort/SortHeadersComponentV2'; +import { ROWS_PER_PAGE_OPTIONS } from '../../../../../../../components/common/queryable/pagination/usePaginationState'; import useBodyItemsStyles from '../../../../../../../components/common/queryable/style/style'; -import { useQueryableWithLocalStorage } from '../../../../../../../components/common/queryable/useQueryableWithLocalStorage'; import { useFormatter } from '../../../../../../../components/i18n'; +import Loader from '../../../../../../../components/Loader'; import { useHelper } from '../../../../../../../store'; -import { type AttackPattern, type EsBase, type ListConfiguration } from '../../../../../../../utils/api-types'; +import { + type AttackPattern, + type EsBase, + type ListConfiguration, type Pagination, +} from '../../../../../../../utils/api-types'; import buildStyles from './elements/ColumnStyles'; import DefaultElementStyles from './elements/DefaultElementStyles'; import EndpointElementStyles from './elements/EndpointElementStyles'; @@ -31,9 +33,6 @@ const useStyles = makeStyles()(() => ({ item: { height: 50 }, })); -// Memoize the initial search pagination to avoid recreation -const INITIAL_SEARCH_PAGINATION = buildSearchPagination({ sorts: initSorting('') }); - // Empty secondary action component to avoid recreation const EmptySecondaryAction = memo(() => <> ); EmptySecondaryAction.displayName = 'EmptySecondaryAction'; @@ -106,9 +105,22 @@ ListWidgetItem.displayName = 'ListWidgetItem'; type Props = { widgetConfig: ListConfiguration; elements: EsBase[]; + currentPageNumber: number; + elementsPerPage: number; + totalElements: number; + onPaginationChange: (paginationInput: Pagination) => void; + contentLoading?: boolean; }; -const ListWidget = ({ widgetConfig, elements }: Props) => { +const ListWidget = ({ + widgetConfig, + elements, + currentPageNumber, + elementsPerPage, + totalElements, + onPaginationChange, + contentLoading = false, +}: Props) => { const { classes } = useStyles(); const { t } = useFormatter(); const bodyItemsStyles = useBodyItemsStyles(); @@ -116,19 +128,24 @@ const ListWidget = ({ widgetConfig, elements }: Props) => { const { attackPatterns } = useHelper((helper: AttackPatternHelper) => ({ attackPatterns: helper.getAttackPatterns() })); + const handleChangePage = (_: unknown, newPage: number) => { + onPaginationChange({ + page: newPage, + size: elementsPerPage, + }); + }; + + const handleChangeRowsPerPage = (event: ChangeEvent) => { + const newRowsPerPage = parseInt(event.target.value, 10); + onPaginationChange({ + page: currentPageNumber, + size: newRowsPerPage, + }); + }; + // Memoize columns array const columns = useMemo(() => widgetConfig.columns ?? [], [widgetConfig.columns]); - // Memoize headers - const headersFromColumns = useMemo( - () => columns.map(col => ({ - field: col, - label: col, - isSortable: false, - })), - [columns], - ); - // Memoize column styles based on entity type const columnStyles = useMemo(() => { const defaultStyles = buildStyles(columns, DefaultElementStyles); @@ -144,8 +161,6 @@ const ListWidget = ({ widgetConfig, elements }: Props) => { } }, [columns, elements]); - const { queryableHelpers } = useQueryableWithLocalStorage('list-widget', INITIAL_SEARCH_PAGINATION); - const onListItemClick = useCallback((element: EsBase): void => { const handler = navigationHandlers[element.base_entity]; handler?.(element, navigate); @@ -161,6 +176,19 @@ const ListWidget = ({ widgetConfig, elements }: Props) => { overflow: 'auto', }} > + {elements.length > 0 + && ( + + )} + { secondaryAction={} > - - )} - /> - {elements.length === 0 &&
{t('No data to display')}
} - {elements.map(element => ( + {contentLoading && } + {!contentLoading && elements.length === 0 &&
{t('No data to display')}
} + {!contentLoading && elements.map(element => ( void; handleChangeRowsPerPage: (rowsPerPage: number) => void; handleChangeTotalElements: (value: number) => void; + handleChangePagination: (object: { + size: number; + page: number; + }) => void; getTotalElements: () => number; + page: number; + elementsPerPage: number; } diff --git a/openaev-front/src/components/common/queryable/pagination/usePaginationState.tsx b/openaev-front/src/components/common/queryable/pagination/usePaginationState.tsx index c83d37db892..9df379f06d1 100644 --- a/openaev-front/src/components/common/queryable/pagination/usePaginationState.tsx +++ b/openaev-front/src/components/common/queryable/pagination/usePaginationState.tsx @@ -1,33 +1,69 @@ import { useCallback, useEffect, useRef, useState } from 'react'; +import { type Pagination } from '../../../../utils/api-types'; import { type PaginationHelpers } from './PaginationHelpers'; export const ROWS_PER_PAGE_OPTIONS = [20, 50, 100]; -const usePaginationState = (initSize?: number, onChange?: (page: number, size: number) => void): PaginationHelpers => { - const [page, setPage] = useState(0); - const [size, setSize] = useState(initSize ?? ROWS_PER_PAGE_OPTIONS[0]); +const usePaginationState = ( + initSize?: number, + onChange?: (page: number, size: number) => void, + persistKey?: string): PaginationHelpers => { + // Load from localStorage if persistKey is provided + const getInitialState = () => { + if (persistKey) { + const saved = localStorage.getItem(persistKey); + if (saved) { + const { page: savedPage, size: savedSize } = JSON.parse(saved); + return { + page: savedPage, + size: savedSize, + }; + } + } + return { + page: 0, + size: initSize ?? ROWS_PER_PAGE_OPTIONS[0], + }; + }; + const initialState = getInitialState(); + const [page, setPage] = useState(initialState.page); + const [size, setSize] = useState(initialState.size); const [totalElements, setTotalElements] = useState(0); // Use ref to store onChange to avoid triggering useEffect when callback reference changes const onChangeRef = useRef(onChange); onChangeRef.current = onChange; - const helpers: PaginationHelpers = { + // Persist to localStorage if persistKey is provided + useEffect(() => { + if (persistKey) { + localStorage.setItem(persistKey, JSON.stringify({ + page, + size, + })); + } + }, [page, size, persistKey]); + + useEffect(() => { + onChangeRef.current?.(page, size); + }, [page, size]); + + return { handleChangePage: useCallback((newPage: number) => setPage(newPage), []), handleChangeRowsPerPage: useCallback((rowsPerPage: number) => { setSize(rowsPerPage); setPage(0); }, []), + handleChangePagination: useCallback(({ page, size }: Pagination) => { + setPage(page); + setSize(size); + }, []), handleChangeTotalElements: useCallback((value: number) => setTotalElements(value), []), getTotalElements: () => totalElements, + page, + elementsPerPage: size, }; - - useEffect(() => { - onChangeRef.current?.(page, size); - }, [page, size]); - - return helpers; }; export default usePaginationState; diff --git a/openaev-front/src/utils/api-types.d.ts b/openaev-front/src/utils/api-types.d.ts index 352a48a6395..446012de79c 100644 --- a/openaev-front/src/utils/api-types.d.ts +++ b/openaev-front/src/utils/api-types.d.ts @@ -2137,6 +2137,13 @@ export interface EngineSortField { fieldName: string; } +export interface EntitiesPaginationInput { + /** Pagination to set (optional) */ + pagination?: Pagination; + /** Parameters to set */ + parameters?: Record; +} + export interface EsAssetGroup { /** @format date-time */ base_created_at?: string; @@ -2254,6 +2261,31 @@ export interface EsEndpoint { endpoint_seen_ip?: string; } +export interface EsEntities { + /** List of data from elasticSearch */ + es_datas: EsBase[]; + /** + * Current page number + * @format int64 + */ + page_number: number; + /** + * Total datas per pages + * @format int64 + */ + page_size: number; + /** + * Total datas + * @format int64 + */ + total: number; + /** + * Current page number + * @format int64 + */ + total_pages: number; +} + export interface EsFinding { /** @format date-time */ base_created_at?: string; @@ -5415,25 +5447,6 @@ export interface PageTeamOutput { totalPages?: number; } -export interface PageTenantOutput { - content?: TenantOutput[]; - empty?: boolean; - first?: boolean; - last?: boolean; - /** @format int32 */ - number?: number; - /** @format int32 */ - numberOfElements?: number; - pageable?: PageableObject; - /** @format int32 */ - size?: number; - sort?: SortObject[]; - /** @format int64 */ - totalElements?: number; - /** @format int32 */ - totalPages?: number; -} - export interface PageUserOutput { content?: UserOutput[]; empty?: boolean; @@ -5484,6 +5497,21 @@ export interface PageableObject { unpaged?: boolean; } +export interface Pagination { + /** + * Page number to get + * @format int32 + * @min 0 + */ + page: number; + /** + * Element number by page + * @format int32 + * @max 1000 + */ + size: number; +} + export type Payload = BasePayload & ( | BasePayloadPayloadTypeMapping<"Command", Command> @@ -7127,20 +7155,6 @@ export interface TeamUpdateInput { team_tags?: string[]; } -export interface TenantInput { - tenant_description?: string; - /** @minLength 1 */ - tenant_name: string; -} - -export interface TenantOutput { - tenant_description?: string; - /** @minLength 1 */ - tenant_id: string; - /** @minLength 1 */ - tenant_name: string; -} - export interface ThemeInput { /** Accent color of the theme */ accent_color?: string; @@ -7707,8 +7721,8 @@ export interface Widget { export interface WidgetConfiguration { /** @minLength 1 */ date_attribute: string; - end?: string | null; - start?: string | null; + end?: string; + start?: string; time_range: | "DEFAULT" | "ALL_TIME" @@ -7762,6 +7776,8 @@ export interface WidgetLayout { export interface WidgetToEntitiesInput { /** Key-value pairs for filtering entities, where the key is the field name and the value is the filter criterion */ filter_values_map?: Record; + /** Pagination for the widget */ + pagination?: Pagination; /** Additional parameters for the widget */ parameters?: Record; /** @@ -7773,7 +7789,7 @@ export interface WidgetToEntitiesInput { export interface WidgetToEntitiesOutput { /** List of entities */ - es_entities?: EsBase[]; + es_entities?: EsEntities; /** List configuration generated based on the input widget id and filter value */ list_configuration?: ListConfiguration; } diff --git a/openaev-model/src/main/java/io/openaev/engine/EngineService.java b/openaev-model/src/main/java/io/openaev/engine/EngineService.java index 714e3a53493..4cd47044833 100644 --- a/openaev-model/src/main/java/io/openaev/engine/EngineService.java +++ b/openaev-model/src/main/java/io/openaev/engine/EngineService.java @@ -8,6 +8,7 @@ import io.openaev.engine.model.EsSearch; import io.openaev.engine.query.EsAvgs; import io.openaev.engine.query.EsCountInterval; +import io.openaev.engine.query.EsEntities; import io.openaev.engine.query.EsSeries; import java.io.IOException; import java.util.List; @@ -117,9 +118,9 @@ EsSeries dateHistogram( * * @param user the user to use * @param runtime the list runtime to use - * @return a list of series + * @return entities result containing data and total count */ - List entities(RawUserAuth user, ListRuntime runtime); + EsEntities entities(RawUserAuth user, ListRuntime runtime); /** * Create the list configuration using entities and filters diff --git a/openaev-model/src/main/java/io/openaev/engine/api/ListRuntime.java b/openaev-model/src/main/java/io/openaev/engine/api/ListRuntime.java index 900fdfd7ef7..ffa353bb92b 100644 --- a/openaev-model/src/main/java/io/openaev/engine/api/ListRuntime.java +++ b/openaev-model/src/main/java/io/openaev/engine/api/ListRuntime.java @@ -1,6 +1,7 @@ package io.openaev.engine.api; import io.openaev.database.model.CustomDashboardParameters; +import io.openaev.utils.pagination.Pagination; import java.util.Map; import lombok.Getter; import lombok.Setter; @@ -10,13 +11,16 @@ public class ListRuntime extends Runtime { private ListConfiguration widget; + private Pagination pagination; public ListRuntime( ListConfiguration widget, Map parameters, - Map definitionParameters) { + Map definitionParameters, + Pagination pagination) { this.widget = widget; this.parameters = parameters; this.definitionParameters = definitionParameters; + this.pagination = pagination != null ? pagination : new Pagination(0, widget.getLimit()); } } diff --git a/openaev-model/src/main/java/io/openaev/engine/query/EsEntities.java b/openaev-model/src/main/java/io/openaev/engine/query/EsEntities.java new file mode 100644 index 00000000000..7b915a7d898 --- /dev/null +++ b/openaev-model/src/main/java/io/openaev/engine/query/EsEntities.java @@ -0,0 +1,41 @@ +package io.openaev.engine.query; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openaev.engine.model.EsBase; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class EsEntities { + @Schema(description = "List of data from elasticSearch") + @JsonProperty("es_datas") + @NotNull + List esDatas = new ArrayList<>(); + + @Schema(description = "Total datas") + @JsonProperty("total") + @NotNull + private long total; + + @Schema(description = "Total datas per pages") + @JsonProperty("page_size") + @NotNull + private long pageSize; + + @Schema(description = "Current page number") + @JsonProperty("page_number") + @NotNull + private long pageNumber; + + @Schema(description = "Current page number") + @JsonProperty("total_pages") + @NotNull + private long totalPages; +} diff --git a/openaev-model/src/main/java/io/openaev/service/ElasticService.java b/openaev-model/src/main/java/io/openaev/service/ElasticService.java index eb819a76433..c417fb1e872 100644 --- a/openaev-model/src/main/java/io/openaev/service/ElasticService.java +++ b/openaev-model/src/main/java/io/openaev/service/ElasticService.java @@ -842,7 +842,7 @@ public List multiDateHistogram(RawUserAuth user, DateHistogramRuntime .toList(); } - public List entities(RawUserAuth user, ListRuntime runtime) { + public EsEntities entities(RawUserAuth user, ListRuntime runtime) { Filters.FilterGroup searchFilters = runtime.getWidget().getPerspective().getFilter(); String entityName = searchFilters.getFilters().stream() @@ -899,18 +899,26 @@ public List entities(RawUserAuth user, ListRuntime runtime) { elasticClient.search( b -> b.index(engineConfig.getIndexPrefix() + "*") - .size(runtime.getWidget().getLimit()) + .size(runtime.getPagination().getSize()) + .from(runtime.getPagination().getPage() * runtime.getPagination().getSize()) .query(finalQuery) .sort(engineSorts), getClassForEntity(entityName)); - return response.hits().hits().stream() - .filter(hit -> hit.source() != null) - .map(hit -> (EsBase) hit.source()) - .toList(); + long total = response.hits().total() != null ? response.hits().total().value() : 0; + return new EsEntities( + response.hits().hits().stream() + .filter(hit -> hit.source() != null) + .map(hit -> (EsBase) hit.source()) + .toList(), + total, + runtime.getPagination().getSize(), + runtime.getPagination().getPage(), + Math.ceilDiv(total, runtime.getPagination().getSize())); + } catch (IOException e) { log.error("query exception: {}", e.getMessage(), e); } - return List.of(); + return new EsEntities(new ArrayList<>(), 0, 0, 0, 0); } private Class getClassForEntity(String entity_name) { diff --git a/openaev-model/src/main/java/io/openaev/service/EsAttackPathService.java b/openaev-model/src/main/java/io/openaev/service/EsAttackPathService.java index 21134a688e6..5a43a857884 100644 --- a/openaev-model/src/main/java/io/openaev/service/EsAttackPathService.java +++ b/openaev-model/src/main/java/io/openaev/service/EsAttackPathService.java @@ -105,7 +105,8 @@ private List fetchSimulationInjectsFromES( config.setTimeRange(CustomDashboardTimeRange.ALL_TIME); return esService - .entities(user, new ListRuntime(config, parameters, definitionParameters)) + .entities(user, new ListRuntime(config, parameters, definitionParameters, null)) + .getEsDatas() .stream() .filter(EsInject.class::isInstance) .map(EsInject.class::cast) diff --git a/openaev-model/src/main/java/io/openaev/service/OpenSearchService.java b/openaev-model/src/main/java/io/openaev/service/OpenSearchService.java index 7a716952107..f590caab1bb 100644 --- a/openaev-model/src/main/java/io/openaev/service/OpenSearchService.java +++ b/openaev-model/src/main/java/io/openaev/service/OpenSearchService.java @@ -925,7 +925,7 @@ public List multiDateHistogram(RawUserAuth user, DateHistogramRuntime .toList(); } - public List entities(RawUserAuth user, ListRuntime runtime) { + public EsEntities entities(RawUserAuth user, ListRuntime runtime) { Filters.FilterGroup searchFilters = runtime.getWidget().getPerspective().getFilter(); String entityName = searchFilters.getFilters().stream() @@ -982,18 +982,26 @@ public List entities(RawUserAuth user, ListRuntime runtime) { openSearchClient.search( b -> b.index(engineConfig.getIndexPrefix() + "*") - .size(runtime.getWidget().getLimit()) + .size(runtime.getPagination().getSize()) + .from(runtime.getPagination().getPage() * runtime.getPagination().getSize()) .query(finalQuery) .sort(engineSorts), getClassForEntity(entityName)); - return response.hits().hits().stream() - .filter(hit -> hit.source() != null) - .map(hit -> (EsBase) hit.source()) - .toList(); + long total = response.hits().total() != null ? response.hits().total().value() : 0; + return new EsEntities( + response.hits().hits().stream() + .filter(hit -> hit.source() != null) + .map(hit -> (EsBase) hit.source()) + .toList(), + total, + runtime.getPagination().getSize(), + runtime.getPagination().getPage(), + Math.ceilDiv(total, runtime.getPagination().getSize())); + } catch (IOException e) { log.error("query exception: {}", e.getMessage(), e); } - return List.of(); + return new EsEntities(new ArrayList<>(), 0, 0, 0, 0); } /** diff --git a/openaev-model/src/main/java/io/openaev/utils/pagination/Pagination.java b/openaev-model/src/main/java/io/openaev/utils/pagination/Pagination.java new file mode 100644 index 00000000000..7e1107207e7 --- /dev/null +++ b/openaev-model/src/main/java/io/openaev/utils/pagination/Pagination.java @@ -0,0 +1,29 @@ +package io.openaev.utils.pagination; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +@Data +public class Pagination { + @Schema(description = "Page number to get") + @NotNull + @Min(0) + @Builder.Default + int page = 0; + + @Schema(description = "Element number by page") + @NotNull + @Max(1000) + @Builder.Default + int size = 20; +} From 31e5df3c208a9eb543ddf1ef673ae91b8628947a Mon Sep 17 00:00:00 2001 From: Marine LM Date: Mon, 16 Feb 2026 09:34:25 +0100 Subject: [PATCH 2/5] [backend] fix: remove committed change Signed-off-by: Marine LM --- openaev-dev/docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openaev-dev/docker-compose.yml b/openaev-dev/docker-compose.yml index a5cf829bdbb..e14769a467c 100644 --- a/openaev-dev/docker-compose.yml +++ b/openaev-dev/docker-compose.yml @@ -244,13 +244,14 @@ services: xtm-composer: image: filigran/xtm-composer:latest platform: linux/amd64 + network_mode: host # allow connecting to non docker local instance environment: - MANAGER__ID=${XTM_COMPOSER_ID} - "MANAGER__NAME=XTM Integrations Manager" - MANAGER__CREDENTIALS_KEY_FILEPATH=/keys/private_key.pem - OPENAEV__ENABLE=true - OPENCTI__ENABLE=false - - OPENAEV__URL=http://192.168.56.1:8080 + - OPENAEV__URL=http://localhost:8080 - OPENAEV__TOKEN=${OPENAEV_ADMIN_TOKEN} - OPENAEV__DAEMON__SELECTOR=docker # Workaround: pending https://github.com/FiligranHQ/xtm-composer/issues/64, From 8f9208c3f88c1f6a12800a56ede6ddffcb14009a Mon Sep 17 00:00:00 2001 From: Marine LM Date: Mon, 16 Feb 2026 09:49:25 +0100 Subject: [PATCH 3/5] [frontend] fix: re-generate api-type Signed-off-by: Marine LM --- openaev-front/src/utils/api-types.d.ts | 37 ++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/openaev-front/src/utils/api-types.d.ts b/openaev-front/src/utils/api-types.d.ts index 446012de79c..382a3b186b4 100644 --- a/openaev-front/src/utils/api-types.d.ts +++ b/openaev-front/src/utils/api-types.d.ts @@ -5447,6 +5447,25 @@ export interface PageTeamOutput { totalPages?: number; } +export interface PageTenantOutput { + content?: TenantOutput[]; + empty?: boolean; + first?: boolean; + last?: boolean; + /** @format int32 */ + number?: number; + /** @format int32 */ + numberOfElements?: number; + pageable?: PageableObject; + /** @format int32 */ + size?: number; + sort?: SortObject[]; + /** @format int64 */ + totalElements?: number; + /** @format int32 */ + totalPages?: number; +} + export interface PageUserOutput { content?: UserOutput[]; empty?: boolean; @@ -7155,6 +7174,20 @@ export interface TeamUpdateInput { team_tags?: string[]; } +export interface TenantInput { + tenant_description?: string; + /** @minLength 1 */ + tenant_name: string; +} + +export interface TenantOutput { + tenant_description?: string; + /** @minLength 1 */ + tenant_id: string; + /** @minLength 1 */ + tenant_name: string; +} + export interface ThemeInput { /** Accent color of the theme */ accent_color?: string; @@ -7721,8 +7754,8 @@ export interface Widget { export interface WidgetConfiguration { /** @minLength 1 */ date_attribute: string; - end?: string; - start?: string; + end?: string | null; + start?: string | null; time_range: | "DEFAULT" | "ALL_TIME" From 9e26110bce03387de299e0ad20ece9adba6cbccf Mon Sep 17 00:00:00 2001 From: Marine LM Date: Mon, 16 Feb 2026 11:58:16 +0100 Subject: [PATCH 4/5] [frontend/backend] fix: fix and add TUs Signed-off-by: Marine LM --- .../CustomDashboardService.java | 16 +++- .../openaev/rest/dashboard/DashboardApi.java | 9 +- .../rest/dashboard/DashboardService.java | 2 +- .../rest/exercise/ExerciseDashboardApi.java | 2 +- .../rest/scenario/ScenarioDashboardApi.java | 2 +- .../rest/settings/PlatformSettingsApi.java | 3 +- .../rest/dashboard/DashboardApiTest.java | 93 ++++++++++++++++--- .../utils/fixtures/EndpointFixture.java | 4 + .../utils/fixtures/PaginationFixture.java | 2 +- .../list/ListWidgetParameters.tsx | 19 ---- .../io/openaev/engine/api/ListRuntime.java | 3 +- .../openaev/utils/pagination/Pagination.java | 3 - 12 files changed, 110 insertions(+), 48 deletions(-) diff --git a/openaev-api/src/main/java/io/openaev/rest/custom_dashboard/CustomDashboardService.java b/openaev-api/src/main/java/io/openaev/rest/custom_dashboard/CustomDashboardService.java index c5c47f488e1..e73b300f78b 100644 --- a/openaev-api/src/main/java/io/openaev/rest/custom_dashboard/CustomDashboardService.java +++ b/openaev-api/src/main/java/io/openaev/rest/custom_dashboard/CustomDashboardService.java @@ -22,10 +22,12 @@ import io.openaev.utils.es.WidgetToEntitiesOutput; import io.openaev.utils.mapper.CustomDashboardMapper; import io.openaev.utils.pagination.SearchPaginationInput; +import jakarta.annotation.Nullable; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.Instant; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -290,12 +292,15 @@ public List dashboardSeriesOnResourceId( public EsEntities dashboardEntitiesOnResourceId( @NotBlank final String resourceId, @NotBlank final String widgetId, - final EntitiesPaginationInput input) { + @Nullable final EntitiesPaginationInput input) { // verify that the widget is in the resource dashboard if (!isWidgetInResourceDashboard(resourceId, widgetId)) { throw new AccessDeniedException("Access denied"); } - return this.dashboardService.entities(widgetId, input.getParameters(), input.getPagination()); + return this.dashboardService.entities( + widgetId, + input == null ? new HashMap<>() : input.getParameters(), + input == null ? null : input.getPagination()); } public WidgetToEntitiesOutput widgetToEntitiesRuntimeOnResourceId( @@ -351,12 +356,15 @@ public List homeDashboardSeries( } public EsEntities homeDashboardEntities( - @NotBlank final String widgetId, final EntitiesPaginationInput input) { + @NotBlank final String widgetId, @Nullable final EntitiesPaginationInput input) { // verify that the widget is in the home dashboard if (!isWidgetInHomeDashboard(widgetId)) { throw new AccessDeniedException("Access denied"); } - return dashboardService.entities(widgetId, input.getParameters(), input.getPagination()); + return dashboardService.entities( + widgetId, + input == null ? new HashMap<>() : input.getParameters(), + input == null ? null : input.getPagination()); } public WidgetToEntitiesOutput homeWidgetToEntitiesRuntimeOnResourceId( diff --git a/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardApi.java b/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardApi.java index daaf84522dd..afb1d73e3b7 100644 --- a/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardApi.java @@ -10,6 +10,7 @@ import io.openaev.utils.es.WidgetToEntitiesInput; import io.openaev.utils.es.WidgetToEntitiesOutput; import jakarta.validation.Valid; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -63,8 +64,12 @@ public List series( actionPerformed = Action.READ, resourceType = ResourceType.DASHBOARD) public EsEntities entities( - @PathVariable final String widgetId, @RequestBody EntitiesPaginationInput input) { - return this.dashboardService.entities(widgetId, input.getParameters(), input.getPagination()); + @PathVariable final String widgetId, + @RequestBody(required = false) EntitiesPaginationInput input) { + return this.dashboardService.entities( + widgetId, + input == null ? new HashMap<>() : input.getParameters(), + input == null ? null : input.getPagination()); } @PostMapping(DASHBOARD_URI + "/entities-runtime/{widgetId}") diff --git a/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardService.java b/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardService.java index c452cf5349b..88ee6e23e64 100644 --- a/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardService.java +++ b/openaev-api/src/main/java/io/openaev/rest/dashboard/DashboardService.java @@ -104,7 +104,7 @@ public List series(String widgetId, Map parameters) { * @return a list of entities retrieved from the engine service */ private EsEntities executeListQuery( - WidgetContext widgetContext, ListConfiguration config, Pagination pagination) { + WidgetContext widgetContext, ListConfiguration config, @Nullable Pagination pagination) { ListRuntime runtime = new ListRuntime( config, widgetContext.parameters(), widgetContext.definitionParameters(), pagination); diff --git a/openaev-api/src/main/java/io/openaev/rest/exercise/ExerciseDashboardApi.java b/openaev-api/src/main/java/io/openaev/rest/exercise/ExerciseDashboardApi.java index 771b9cf6222..63917ecab69 100644 --- a/openaev-api/src/main/java/io/openaev/rest/exercise/ExerciseDashboardApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/exercise/ExerciseDashboardApi.java @@ -103,7 +103,7 @@ public EsEntities dashboardEntities( public WidgetToEntitiesOutput widgetToEntitiesRuntime( @PathVariable final String simulationId, @PathVariable final String widgetId, - @Valid @RequestBody WidgetToEntitiesInput input) { + @Valid @RequestBody(required = false) WidgetToEntitiesInput input) { return this.customDashboardService.widgetToEntitiesRuntimeOnResourceId( simulationId, widgetId, input); } diff --git a/openaev-api/src/main/java/io/openaev/rest/scenario/ScenarioDashboardApi.java b/openaev-api/src/main/java/io/openaev/rest/scenario/ScenarioDashboardApi.java index 550a7c4fab7..7341a48d650 100644 --- a/openaev-api/src/main/java/io/openaev/rest/scenario/ScenarioDashboardApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/scenario/ScenarioDashboardApi.java @@ -102,7 +102,7 @@ public EsEntities dashboardEntities( public WidgetToEntitiesOutput widgetToEntitiesRuntime( @PathVariable final String scenarioId, @PathVariable final String widgetId, - @Valid @RequestBody WidgetToEntitiesInput input) { + @Valid @RequestBody(required = false) WidgetToEntitiesInput input) { return this.customDashboardService.widgetToEntitiesRuntimeOnResourceId( scenarioId, widgetId, input); } diff --git a/openaev-api/src/main/java/io/openaev/rest/settings/PlatformSettingsApi.java b/openaev-api/src/main/java/io/openaev/rest/settings/PlatformSettingsApi.java index 7c67e464849..39f7e64951a 100644 --- a/openaev-api/src/main/java/io/openaev/rest/settings/PlatformSettingsApi.java +++ b/openaev-api/src/main/java/io/openaev/rest/settings/PlatformSettingsApi.java @@ -165,7 +165,8 @@ public List homeDashboardSeries( @PostMapping("/home-dashboard/entities/{widgetId}") @AccessControl(actionPerformed = Action.READ, resourceType = ResourceType.PLATFORM_SETTING) public EsEntities homeDashboardEntities( - @PathVariable final String widgetId, @RequestBody EntitiesPaginationInput input) { + @PathVariable final String widgetId, + @RequestBody(required = false) EntitiesPaginationInput input) { return customDashboardService.homeDashboardEntities(widgetId, input); } diff --git a/openaev-api/src/test/java/io/openaev/rest/dashboard/DashboardApiTest.java b/openaev-api/src/test/java/io/openaev/rest/dashboard/DashboardApiTest.java index 3b8eaac9c2b..e9f6c130d3c 100644 --- a/openaev-api/src/test/java/io/openaev/rest/dashboard/DashboardApiTest.java +++ b/openaev-api/src/test/java/io/openaev/rest/dashboard/DashboardApiTest.java @@ -22,11 +22,13 @@ import io.openaev.engine.api.ListConfiguration; import io.openaev.engine.api.SortDirection; import io.openaev.utils.CustomDashboardTimeRange; +import io.openaev.utils.es.EntitiesPaginationInput; import io.openaev.utils.es.WidgetToEntitiesInput; import io.openaev.utils.fixtures.*; import io.openaev.utils.fixtures.composers.*; import io.openaev.utils.fixtures.files.AttackPatternFixture; import io.openaev.utils.mockUser.WithMockUser; +import io.openaev.utils.pagination.Pagination; import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import java.io.IOException; @@ -109,7 +111,8 @@ void WhenNoSpecificFilter_ReturnAllEntitiesFromDimension() throws Exception { .getResponse() .getContentAsString(); - assertThatJson(response).node("[0].base_id").isEqualTo(ep.getId()); + assertThatJson(response).node("es_datas[0].base_id").isEqualTo(ep.getId()); + assertThatJson(response).node("total").isEqualTo(1); } @Test @@ -160,9 +163,10 @@ void WhenSortingIsSpecified_ReturnEntitiesSortedAccordingly() throws Exception { .getResponse() .getContentAsString(); - assertThatJson(response).node("[0].base_id").isEqualTo(epWrapper1.get().getId()); - assertThatJson(response).node("[1].base_id").isEqualTo(epWrapper2.get().getId()); - assertThatJson(response).node("[2].base_id").isEqualTo(epWrapper3.get().getId()); + assertThatJson(response).node("total").isEqualTo(3); + assertThatJson(response).node("es_datas[0].base_id").isEqualTo(epWrapper1.get().getId()); + assertThatJson(response).node("es_datas[1].base_id").isEqualTo(epWrapper2.get().getId()); + assertThatJson(response).node("es_datas[2].base_id").isEqualTo(epWrapper3.get().getId()); } @Test @@ -253,26 +257,85 @@ void WhenBindingWithDashboardParam_ParamIsAppliedToReturnedCollection() throws E // completes before the data is available in the system Thread.sleep(1000); + EntitiesPaginationInput input = new EntitiesPaginationInput(); + Map params = new HashMap<>(); + params.put(paramWrapper.get().getId(), exerciseWrapper1.get().getId()); + input.setParameters(params); String response = mvc.perform( post(DASHBOARD_URI + "/entities/" + widget.getId()) .contentType(MediaType.APPLICATION_JSON) - .content( - "{\"%s\":\"%s\"}" - .formatted( - paramWrapper.get().getId(), exerciseWrapper1.get().getId()))) + .content(asJsonString(input))) .andExpect(status().isOk()) .andReturn() .getResponse() .getContentAsString(); assertThatJson(response) - .node("[0].vulnerable_endpoint_id") + .node("es_datas[0].vulnerable_endpoint_id") .isEqualTo(epWrapper2.get().getId()); assertThatJson(response) - .node("[1].vulnerable_endpoint_id") + .node("es_datas[1].vulnerable_endpoint_id") .isEqualTo(epWrapper1.get().getId()); - assertThatJson(response).isArray().size().isEqualTo(2); + assertThatJson(response).node("es_datas").isArray().size().isEqualTo(2); + assertThatJson(response).node("total").isEqualTo(2); + } + + @Test + @DisplayName("When paginating is specified, return entities paginated accordingly") + void WhenPaginatingIsSpecified_ReturnEntitiesPaginatedAccordingly() throws Exception { + endpointComposer.forEndpoint(EndpointFixture.createEndpoint("A")).persist().get(); + endpointComposer.forEndpoint(EndpointFixture.createEndpoint("B")).persist().get(); + Endpoint epC = + endpointComposer.forEndpoint(EndpointFixture.createEndpoint("C")).persist().get(); + Endpoint epD = + endpointComposer.forEndpoint(EndpointFixture.createEndpoint("D")).persist().get(); + endpointComposer.forEndpoint(EndpointFixture.createEndpoint("E")).persist().get(); + + Widget listWidget = WidgetFixture.createListWidgetWithEntity("endpoint"); + EngineSortField sortField = new EngineSortField(); + sortField.setFieldName("endpoint_name"); + sortField.setDirection(SortDirection.ASC); + ((ListConfiguration) listWidget.getWidgetConfiguration()).setSorts(List.of(sortField)); + Widget widget = + widgetComposer + .forWidget(listWidget) + .withCustomDashboard( + customDashboardComposer.forCustomDashboard( + CustomDashboardFixture.createCustomDashboardWithDefaultParams())) + .persist() + .get(); + + // force persistence + entityManager.flush(); + entityManager.clear(); + engineService.bulkProcessing(engineContext.getModels().stream()); + // elastic needs to process the data; it does so async, so the method above + // completes before the data is available in the system + Thread.sleep(1000); + + EntitiesPaginationInput input = new EntitiesPaginationInput(); + Pagination pagination = new Pagination(); + pagination.setPage(1); + pagination.setSize(2); + input.setPagination(pagination); + + String response = + mvc.perform( + post(DASHBOARD_URI + "/entities/" + widget.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(input))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThatJson(response).node("total").isEqualTo(5); + assertThatJson(response).node("page_number").isEqualTo(1); + assertThatJson(response).node("page_size").isEqualTo(2); + assertThatJson(response).node("total_pages").isEqualTo(3); + assertThatJson(response).node("es_datas[0].base_id").isEqualTo(epC.getId()); + assertThatJson(response).node("es_datas[1].base_id").isEqualTo(epD.getId()); } } @@ -820,7 +883,8 @@ void given_structuralEndpointHistogram_should_returnListOfWindowsEndpoint() thro assertThatJson(filter).node("key").isEqualTo("endpoint_platform"); assertThatJson(filter).node("values").isArray().containsExactly("Windows"); }); - assertThatJson(response).node("es_entities").isArray().size().isEqualTo(2); + assertThatJson(response).node("es_entities.total").isEqualTo(2); + assertThatJson(response).node("es_entities.es_datas").isArray().size().isEqualTo(2); } @Test @@ -907,11 +971,12 @@ void given_securityCoverageWidget_should_returnListOfInjectExpectations() throws .containsExactly("expectation-inject"); }); assertThatJson(response) - .node("es_entities") + .node("es_entities.es_datas") .isArray() .hasSize(6) .extracting("base_inject_side") .containsOnly(inject1.getId(), inject2.getId(), inject3.getId()); + assertThatJson(response).node("es_entities.total").isEqualTo(6); } @Test @@ -988,7 +1053,7 @@ void given_securityDomainWidget_should_returnListOfExpectationFilteredByDomain() .containsExactly(networkDomain.getId()); }); - assertThatJson(response).node("es_entities").isArray().hasSize(2); + assertThatJson(response).node("es_entities.es_datas").isArray().hasSize(2); } } } diff --git a/openaev-api/src/test/java/io/openaev/utils/fixtures/EndpointFixture.java b/openaev-api/src/test/java/io/openaev/utils/fixtures/EndpointFixture.java index 3a2356d9117..c1ed62b435d 100644 --- a/openaev-api/src/test/java/io/openaev/utils/fixtures/EndpointFixture.java +++ b/openaev-api/src/test/java/io/openaev/utils/fixtures/EndpointFixture.java @@ -73,6 +73,10 @@ public static Endpoint createEndpoint() { return baseEndpoint("Endpoint test", Endpoint.PLATFORM_TYPE.Windows); } + public static Endpoint createEndpoint(String name) { + return baseEndpoint(name, Endpoint.PLATFORM_TYPE.Windows); + } + public static Endpoint createEndpointWithPlatform(String name, Endpoint.PLATFORM_TYPE platform) { return baseEndpoint(name, platform); } diff --git a/openaev-api/src/test/java/io/openaev/utils/fixtures/PaginationFixture.java b/openaev-api/src/test/java/io/openaev/utils/fixtures/PaginationFixture.java index ae9e6c2d7b6..1981fd45070 100644 --- a/openaev-api/src/test/java/io/openaev/utils/fixtures/PaginationFixture.java +++ b/openaev-api/src/test/java/io/openaev/utils/fixtures/PaginationFixture.java @@ -10,7 +10,7 @@ public class PaginationFixture { - public static SearchPaginationInput.SearchPaginationInputBuilder getDefault() { + public static SearchPaginationInput.SearchPaginationInputBuilder getDefault() { return SearchPaginationInput.builder().page(0).size(10); } diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/configuration/list/ListWidgetParameters.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/configuration/list/ListWidgetParameters.tsx index 279c404246a..74aacac24f5 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/configuration/list/ListWidgetParameters.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgets/configuration/list/ListWidgetParameters.tsx @@ -137,25 +137,6 @@ const ListWidgetParameters = (props: Props) => { )} /> - ( - field.onChange(e.target.value === '' ? undefined : Number(e.target.value))} - error={!!fieldState.error} - helperText={fieldState.error?.message} - required - /> - )} - /> diff --git a/openaev-model/src/main/java/io/openaev/engine/api/ListRuntime.java b/openaev-model/src/main/java/io/openaev/engine/api/ListRuntime.java index ffa353bb92b..9563827eddd 100644 --- a/openaev-model/src/main/java/io/openaev/engine/api/ListRuntime.java +++ b/openaev-model/src/main/java/io/openaev/engine/api/ListRuntime.java @@ -2,6 +2,7 @@ import io.openaev.database.model.CustomDashboardParameters; import io.openaev.utils.pagination.Pagination; +import jakarta.annotation.Nullable; import java.util.Map; import lombok.Getter; import lombok.Setter; @@ -17,7 +18,7 @@ public ListRuntime( ListConfiguration widget, Map parameters, Map definitionParameters, - Pagination pagination) { + @Nullable Pagination pagination) { this.widget = widget; this.parameters = parameters; this.definitionParameters = definitionParameters; diff --git a/openaev-model/src/main/java/io/openaev/utils/pagination/Pagination.java b/openaev-model/src/main/java/io/openaev/utils/pagination/Pagination.java index 7e1107207e7..0c9f982b4c9 100644 --- a/openaev-model/src/main/java/io/openaev/utils/pagination/Pagination.java +++ b/openaev-model/src/main/java/io/openaev/utils/pagination/Pagination.java @@ -5,7 +5,6 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; @@ -18,12 +17,10 @@ public class Pagination { @Schema(description = "Page number to get") @NotNull @Min(0) - @Builder.Default int page = 0; @Schema(description = "Element number by page") @NotNull @Max(1000) - @Builder.Default int size = 20; } From 0e4d989287d2311c43ef54c3fed251972f2b47ab Mon Sep 17 00:00:00 2001 From: Marine LM Date: Fri, 20 Feb 2026 11:46:56 +0100 Subject: [PATCH 5/5] [frontend] feat: fix condition Signed-off-by: Marine LM --- .../custom_dashboards/widgetDataDrawer/WidgetDataDrawer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgetDataDrawer/WidgetDataDrawer.tsx b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgetDataDrawer/WidgetDataDrawer.tsx index 896fa6f1069..f1eff8d6085 100644 --- a/openaev-front/src/admin/components/workspaces/custom_dashboards/widgetDataDrawer/WidgetDataDrawer.tsx +++ b/openaev-front/src/admin/components/workspaces/custom_dashboards/widgetDataDrawer/WidgetDataDrawer.tsx @@ -55,7 +55,7 @@ const WidgetDataDrawer = () => { }, [widgetId, filterValues]); useEffect(() => { - if (!customDashboard || !widgetId || filterValues == null || !seriesIndex) { + if (!customDashboard || !widgetId || filterValues == null) { setOpen(false); return; }