From d27f9b42bf54cbc394ddcac9c861df52e817fd95 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Fri, 27 Sep 2024 14:31:44 +0200 Subject: [PATCH 01/34] [frontend/backend] Chaining injects logically --- ...191705__Add_table_inject_dependencies.java | 49 ++ .../io/openbas/rest/inject/InjectApi.java | 54 ++- .../openbas/rest/inject/form/InjectInput.java | 3 +- .../rest/inject/output/InjectOutput.java | 16 +- .../scheduler/jobs/InjectsExecutionJob.java | 111 ++++- .../io/openbas/service/InjectService.java | 14 +- .../service/ScenarioToExerciseService.java | 2 +- .../common/injects/InjectChainsForm.js | 452 +++++++++++++++++- .../common/injects/UpdateInjectDetails.js | 4 +- .../injects/UpdateInjectLogicalChains.js | 33 +- .../common/injects/chaining/ChainingUtils.tsx | 13 + .../src/components/ChainedTimeline.tsx | 45 +- .../src/components/common/chips/ChipUtils.tsx | 12 + .../components/common/chips/ClickableChip.tsx | 181 +++++++ .../common/chips/ClickableChipPopover.tsx | 73 +++ .../common/chips/ClickableModeChip.tsx | 55 +++ openbas-front/src/utils/api-types.d.ts | 20 +- .../io/openbas/database/model/Inject.java | 10 +- .../database/model/InjectDependency.java | 52 ++ .../database/model/InjectDependencyId.java | 50 ++ .../InjectDependenciesRepository.java | 37 ++ .../io/openbas/helper/InjectModelHelper.java | 9 +- 22 files changed, 1206 insertions(+), 89 deletions(-) create mode 100644 openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java create mode 100644 openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx create mode 100644 openbas-front/src/components/common/chips/ChipUtils.tsx create mode 100644 openbas-front/src/components/common/chips/ClickableChip.tsx create mode 100644 openbas-front/src/components/common/chips/ClickableChipPopover.tsx create mode 100644 openbas-front/src/components/common/chips/ClickableModeChip.tsx create mode 100644 openbas-model/src/main/java/io/openbas/database/model/InjectDependency.java create mode 100644 openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java create mode 100644 openbas-model/src/main/java/io/openbas/database/repository/InjectDependenciesRepository.java diff --git a/openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java b/openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java new file mode 100644 index 0000000000..c84971f749 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java @@ -0,0 +1,49 @@ +package io.openbas.migration; + +import io.openbas.database.model.Variable; +import org.flywaydb.core.api.migration.BaseJavaMigration; +import org.flywaydb.core.api.migration.Context; +import org.springframework.stereotype.Component; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; + +@Component +public class V3_202409191705__Add_table_inject_dependencies extends BaseJavaMigration { + + @Override + public void migrate(Context context) throws Exception { + Statement select = context.getConnection().createStatement(); + select.execute(""" + CREATE TABLE injects_dependencies ( + inject_parent_id VARCHAR(255) NOT NULL REFERENCES injects(inject_id) ON DELETE CASCADE, + inject_children_id VARCHAR(255) NOT NULL REFERENCES injects(inject_id) ON DELETE CASCADE, + dependency_condition VARCHAR(255) NOT NULL, + dependency_created_at TIMESTAMP DEFAULT now(), + dependency_updated_at TIMESTAMP DEFAULT now(), + PRIMARY KEY(inject_parent_id, inject_children_id) + ); + CREATE INDEX idx_injects_dependencies ON injects_dependencies(inject_children_id); + """); + + // Migration datas + ResultSet results = select.executeQuery("SELECT * FROM injects WHERE inject_depends_from_another IS NOT NULL"); + PreparedStatement statement = context.getConnection().prepareStatement( + """ + INSERT INTO injects_dependencies(inject_parent_id, inject_children_id, dependency_condition) + VALUES (?, ?, ?) + """ + ); + while (results.next()) { + String injectId = results.getString("inject_id"); + String parentId = results.getString("inject_depends_from_another"); + String condition = String.format("%s-Execution-Success == true", parentId); + statement.setString(1, parentId); + statement.setString(2, injectId); + statement.setString(3, condition); + statement.addBatch(); + } + statement.executeBatch(); + } +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java index 690e765168..08f5437b53 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java @@ -41,6 +41,7 @@ import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -87,6 +88,7 @@ public class InjectApi extends RestBehavior { private final InjectService injectService; private final AtomicTestingService atomicTestingService; private final InjectDuplicateService injectDuplicateService; + private final InjectDependenciesRepository injectDependenciesRepository; // -- INJECTS -- @@ -297,7 +299,15 @@ public Inject createInjectForExercise(@PathVariable String exerciseId, @Valid @R inject.setUser(userRepository.findById(currentUser().getId()).orElseThrow(ElementNotFoundException::new)); inject.setExercise(exercise); // Set dependencies - inject.setDependsOn(resolveOptionalRelation(input.getDependsOn(), injectRepository)); + if(input.getDependsOn() != null) { + inject.setDependsOn(input.getDependsOn().entrySet().stream().map(entry -> { + InjectDependency injectDependency = new InjectDependency(); + injectDependency.getCompositeId().setInjectChildrenId(injectRepository.findById(entry.getKey()).orElse(null)); + injectDependency.getCompositeId().setInjectParentId(inject); + injectDependency.setCondition(entry.getValue()); + return injectDependency; + }).toList()); + } inject.setTeams(fromIterable(teamRepository.findAllById(input.getTeams()))); inject.setAssets(fromIterable(assetService.assets(input.getAssets()))); inject.setAssetGroups(fromIterable(assetGroupService.assetGroups(input.getAssetGroups()))); @@ -467,7 +477,15 @@ public Inject createInjectForScenario( inject.setUser(this.userRepository.findById(currentUser().getId()).orElseThrow(ElementNotFoundException::new)); inject.setScenario(scenario); // Set dependencies - inject.setDependsOn(resolveOptionalRelation(input.getDependsOn(), this.injectRepository)); + if(input.getDependsOn() != null) { + inject.setDependsOn(input.getDependsOn().entrySet().stream().map(entry -> { + InjectDependency injectDependency = new InjectDependency(); + injectDependency.getCompositeId().setInjectChildrenId(injectRepository.findById(entry.getKey()).orElse(null)); + injectDependency.getCompositeId().setInjectParentId(inject); + injectDependency.setCondition(entry.getValue()); + return injectDependency; + }).toList()); + } inject.setTeams(fromIterable(teamRepository.findAllById(input.getTeams()))); inject.setAssets(fromIterable(assetService.assets(input.getAssets()))); inject.setAssetGroups(fromIterable(assetGroupService.assetGroups(input.getAssetGroups()))); @@ -575,7 +593,37 @@ private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInpu inject.setUpdateAttributes(input); // Set dependencies - inject.setDependsOn(updateRelation(input.getDependsOn(), inject.getDependsOn(), this.injectRepository)); + if(input.getDependsOn() != null) { + input.getDependsOn().entrySet().forEach(entry -> { + Optional existingDependency = inject.getDependsOn().stream() + .filter(injectDependency -> injectDependency.getCompositeId().getInjectParentId().getId().equals(entry.getKey())) + .findFirst(); + if(existingDependency.isPresent()) { + existingDependency.get().setCondition(entry.getValue()); + } else { + InjectDependency injectDependency = new InjectDependency(); + injectDependency.getCompositeId().setInjectChildrenId(inject); + injectDependency.getCompositeId().setInjectParentId(injectRepository.findById(entry.getKey()).orElse(null)); + injectDependency.setCondition(entry.getValue()); + inject.getDependsOn().add(injectDependency); + } + }); + } + + List injectDepencyToRemove = new ArrayList<>(); + if(input.getDependsOn() != null) { + inject.getDependsOn().forEach( + injectDependency -> { + if (!input.getDependsOn().keySet().contains(injectDependency.getCompositeId().getInjectParentId().getId())) { + injectDepencyToRemove.add(injectDependency); + } + } + ); + } else { + injectDepencyToRemove.addAll(inject.getDependsOn()); + } + inject.getDependsOn().removeAll(injectDepencyToRemove); + inject.setTeams(fromIterable(this.teamRepository.findAllById(input.getTeams()))); inject.setAssets(fromIterable(this.assetService.assets(input.getAssets()))); inject.setAssetGroups(fromIterable(this.assetGroupService.assetGroups(input.getAssetGroups()))); diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java index 8571fc4003..264c1c770b 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; @Setter @Getter @@ -28,7 +29,7 @@ public class InjectInput { private ObjectNode content; @JsonProperty("inject_depends_on") - private String dependsOn; + private Map dependsOn; @JsonProperty("inject_depends_duration") private Long dependsDuration; diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java index 38278ae5df..a27343b312 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.openbas.database.model.InjectDependency; import io.openbas.database.model.InjectorContract; import io.openbas.helper.InjectModelHelper; import io.openbas.injectors.email.EmailContract; @@ -12,6 +13,8 @@ import lombok.Data; import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Data public class InjectOutput { @@ -39,7 +42,7 @@ public class InjectOutput { private Long dependsDuration; @JsonProperty("inject_depends_on") - private String dependsOn; + private Map dependsOn; @JsonProperty("inject_injector_contract") private InjectorContract injectorContract; @@ -79,20 +82,19 @@ public InjectOutput( String exerciseId, String scenarioId, Long dependsDuration, - String dependsOn, InjectorContract injectorContract, String[] tags, String[] teams, String[] assets, String[] assetGroups, - String injectType) { + String injectType, + InjectDependency injectDependency) { this.id = id; this.title = title; this.enabled = enabled; this.exercise = exerciseId; this.scenario = scenarioId; this.dependsDuration = dependsDuration; - this.dependsOn = dependsOn; this.injectorContract = injectorContract; this.tags = tags != null ? new HashSet<>(Arrays.asList(tags)) : new HashSet<>(); @@ -111,5 +113,11 @@ public InjectOutput( this.injectType = injectType; this.teams = teams != null ? new ArrayList<>(Arrays.asList(teams)) : new ArrayList<>(); this.content = content; + + if (injectDependency != null) { + this.dependsOn = Stream.of(injectDependency) + .collect(Collectors.toMap(injectDep -> injectDep.getCompositeId().getInjectParentId().getId(), InjectDependency::getCondition)); + } + } } diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index 784c17e599..b7378b61c4 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -1,30 +1,47 @@ package io.openbas.scheduler.jobs; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.openbas.asset.QueueService; import io.openbas.database.model.*; import io.openbas.database.repository.*; import io.openbas.execution.ExecutableInject; import io.openbas.execution.ExecutionExecutorService; import io.openbas.helper.InjectHelper; +import io.openbas.inject_expectation.InjectExpectationService; +import io.openbas.injector_contract.ContractType; +import io.openbas.model.Expectation; import io.openbas.service.AtomicTestingService; +import io.openbas.service.ExerciseExpectationService; import jakarta.annotation.Resource; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import java.time.Instant; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import static java.time.Instant.now; import static java.util.stream.Collectors.groupingBy; @@ -47,10 +64,16 @@ public class InjectsExecutionJob implements Job { private final QueueService queueService; private final ExecutionExecutorService executionExecutorService; private final AtomicTestingService atomicTestingService; + private final InjectDependenciesRepository injectDependenciesRepository; + private final ExerciseExpectationService exerciseExpectationService; + + private static final String EXPECTATIONS = "expectations"; private final List executionStatusesNotReady = List.of(ExecutionStatus.QUEUING, ExecutionStatus.DRAFT, ExecutionStatus.EXECUTING, ExecutionStatus.PENDING); + private final List expectationStatusesSuccess = List.of(InjectExpectation.EXPECTATION_STATUS.SUCCESS); + @Resource protected ObjectMapper mapper; @@ -148,24 +171,23 @@ private void executeInject(ExecutableInject executableInject) { Inject inject = executableInject.getInjection().getInject(); // We are now checking if we depend on another inject and if it did not failed - if (inject.getDependsOn() != null - && inject.getDependsOn().getStatus().isPresent() - && ( inject.getDependsOn().getStatus().get().getName().equals(ExecutionStatus.ERROR) - || executionStatusesNotReady.contains(inject.getDependsOn().getStatus().get().getName()))) { + Optional> errorMessages = getErrorMessagesPreExecution(executableInject.getExercise().getId(), inject); + if (errorMessages.isPresent()) { InjectStatus status = new InjectStatus(); if (inject.getStatus().isEmpty()) { status.setInject(inject); } else { status = inject.getStatus().get(); } - String errorMsg = inject.getDependsOn().getStatus().get().getName().equals(ExecutionStatus.ERROR) ? - "The inject is depending on another inject that failed" - : "The inject is depending on another inject that is not executed yet"; - status.getTraces().add(InjectStatusExecution.traceError(errorMsg)); - status.setName(ExecutionStatus.ERROR); - status.setTrackingSentDate(Instant.now()); - status.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); - injectStatusRepository.save(status); + + InjectStatus finalStatus = status; + errorMessages.get().forEach( + errorMsg -> finalStatus.getTraces().add(InjectStatusExecution.traceError(errorMsg)) + ); + finalStatus.setName(ExecutionStatus.ERROR); + finalStatus.setTrackingSentDate(Instant.now()); + finalStatus.setCommandsLines(atomicTestingService.getCommandsLinesFromInject(inject)); + injectStatusRepository.save(finalStatus); } else { inject.getInjectorContract().ifPresent(injectorContract -> { @@ -225,6 +247,67 @@ private void executeInject(ExecutableInject executableInject) { } } + private Optional> getErrorMessagesPreExecution(String exerciseId, Inject inject) { + List injectDependencies = injectDependenciesRepository.findParents(List.of(inject.getId())); + if (!injectDependencies.isEmpty()) { + List parents = StreamSupport.stream(injectRepository.findAllById(injectDependencies.stream() + .map(injectDependency -> injectDependency.getCompositeId().getInjectParentId().getId()).toList()).spliterator(), false) + .collect(Collectors.toList()); + + Map mapCondition = getStringBooleanMap(parents, exerciseId, injectDependencies); + + List results = null; + + for (InjectDependency injectDependency : injectDependencies) { + String expressionToEvaluate = injectDependency.getCondition() + .replaceAll("(.*-Execution-Success)", "#this['$1']"); + + ExpressionParser parser = new SpelExpressionParser(); + EvaluationContext context = new StandardEvaluationContext(mapCondition); + Expression exp = parser.parseExpression(String.format(expressionToEvaluate, + injectDependency.getCompositeId().getInjectParentId() + "-Execution-Success")); + boolean canBeExecuted = Boolean.TRUE.equals(exp.getValue(mapCondition, Boolean.class)); + if (!canBeExecuted) { + if (results == null) { + results = new ArrayList<>(); + } + results.add("The conditions to launch this inject are not met"); + } + } + return results == null ? Optional.empty() : Optional.of(results); + } + return Optional.empty(); + } + + private @NotNull Map getStringBooleanMap(List parents, String exerciseId, List injectDependencies) { + Map mapCondition = new HashMap<>(); + + List expectations = this.exerciseExpectationService.injectExpectations(exerciseId); + + injectDependencies.forEach(injectDependency -> { + List splittedConditions = List.of(injectDependency.getCondition().split("&&|\\|\\|")); + splittedConditions.forEach(condition -> { + mapCondition.put(injectDependency.getCondition().split("==")[0].trim(), false); + }); + }); + + parents.forEach(parent -> { + mapCondition.put(parent.getId() + "-Execution-Success", + parent.getStatus().isPresent() + && !parent.getStatus().get().getName().equals(ExecutionStatus.ERROR) + && !executionStatusesNotReady.contains(parent.getStatus().get().getName())); + + parent.getExpectations().forEach(injectExpectation -> { + String name = String.format("%s-%s-Success", parent.getId(), StringUtils.capitalize(injectExpectation.getType().toString().toLowerCase())); + if(injectExpectation.getType().equals(InjectExpectation.EXPECTATION_TYPE.MANUAL)) { + name = String.format("%s-%s-Success", parent.getId(), injectExpectation.getName()); + } + mapCondition.put(name, expectationStatusesSuccess.contains(injectExpectation.getResponse())); + }); + }); + return mapCondition; + } + public void updateExercise(String exerciseId) { Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); exercise.setUpdatedAt(now()); diff --git a/openbas-api/src/main/java/io/openbas/service/InjectService.java b/openbas-api/src/main/java/io/openbas/service/InjectService.java index d1c6c905fc..c6e825984a 100644 --- a/openbas-api/src/main/java/io/openbas/service/InjectService.java +++ b/openbas-api/src/main/java/io/openbas/service/InjectService.java @@ -1199,7 +1199,8 @@ private void selectForInject(CriteriaBuilder cb, CriteriaQuery cq, Root injectScenarioJoin = createLeftJoin(injectRoot, "scenario"); Join injectorContractJoin = createLeftJoin(injectRoot, "injectorContract"); Join injectorJoin = injectorContractJoin.join("injector", JoinType.LEFT); - Join injectDependsJoin = createLeftJoin(injectRoot, "dependsOn"); + Join injectDependency = createLeftJoin(injectRoot, "dependsOn"); + // Array aggregations Expression tagIdsExpression = createJoinArrayAggOnId(cb, injectRoot, "tags"); Expression teamIdsExpression = createJoinArrayAggOnId(cb, injectRoot, "teams"); @@ -1216,13 +1217,13 @@ private void selectForInject(CriteriaBuilder cb, CriteriaQuery cq, Root cq, Root execInject(TypedQuery query) { tuple.get("inject_exercise", String.class), tuple.get("inject_scenario", String.class), tuple.get("inject_depends_duration", Long.class), - tuple.get("inject_depends_from_another", String.class), tuple.get("inject_injector_contract", InjectorContract.class), tuple.get("inject_tags", String[].class), tuple.get("inject_teams", String[].class), tuple.get("inject_assets", String[].class), tuple.get("inject_asset_groups", String[].class), - tuple.get("inject_type", String.class) + tuple.get("inject_type", String.class), + tuple.get("inject_depends_on", InjectDependency.class) )) .toList(); } diff --git a/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java b/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java index 7669482a2c..14daf17b13 100644 --- a/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java +++ b/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java @@ -247,7 +247,7 @@ public Exercise toExercise( scenarioInjects.forEach(scenarioInject -> { if(scenarioInject.getDependsOn() != null) { Inject injectToUpdate = mapExerciseInjectsByScenarioInject.get(scenarioInject.getId()); - injectToUpdate.setDependsOn(mapExerciseInjectsByScenarioInject.get(scenarioInject.getDependsOn().getId())); + injectToUpdate.setDependsOn(scenarioInject.getDependsOn()); this.injectRepository.save(injectToUpdate); } }); diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js index 90ee59112e..85168c1c8b 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js @@ -1,8 +1,10 @@ import React, { useState } from 'react'; import { makeStyles } from '@mui/styles'; -import { Accordion, AccordionDetails, AccordionSummary, FormControl, IconButton, InputLabel, MenuItem, Select, Tooltip, Typography } from '@mui/material'; +import { Accordion, AccordionDetails, AccordionSummary, Box, Button, FormControl, IconButton, InputLabel, MenuItem, Select, Tooltip, Typography } from '@mui/material'; import { Add, DeleteOutlined, ExpandMore } from '@mui/icons-material'; import { useFormatter } from '../../../../components/i18n'; +import ClickableModeChip from '../../../../components/common/chips/ClickableModeChip'; +import ClickableChip from '../../../../components/common/chips/ClickableChip'; const useStyles = makeStyles(() => ({ container: { @@ -14,6 +16,9 @@ const useStyles = makeStyles(() => ({ alignItems: 'center', marginTop: 20, }, + labelExecutionCondition: { + color: '#7c8088', + }, })); const InjectForm = ({ @@ -23,20 +28,82 @@ const InjectForm = ({ }) => { const classes = useStyles(); const { t } = useFormatter(); - const [parents, setParents] = useState( - injects.filter((currentInject) => currentInject.inject_id === values.inject_depends_on) + injects.filter((currentInject) => values.inject_depends_on !== null + && values.inject_depends_on[currentInject.inject_id] !== undefined) .map((inject, index) => { return { inject, index }; }), ); const [childrens, setChildrens] = useState( - injects.filter((currentInject) => currentInject.inject_depends_on === values.inject_id) + injects.filter((currentInject) => currentInject.inject_depends_on !== null + && currentInject.inject_depends_on[values.inject_id] !== undefined) .map((inject, index) => { return { inject, index }; }), ); + const getConditionContentParent = (injectDependsOn) => { + const breakpointAndOr = /&&|\|\|/gm; + const breakpointValue = /==/gm; + const typeFromName = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-(.*)-Success/mg; + const conditions = []; + for (const dependency in injectDependsOn) { + if (Object.hasOwn(injectDependsOn, dependency)) { + const condition = injectDependsOn[dependency]; + const splittedConditions = condition.split(breakpointAndOr); + conditions.push({ + parentId: dependency, + mode: condition.includes('||') ? 'or' : 'and', + conditionElement: splittedConditions.map((splitedCondition, index) => { + const key = Array.from(splitedCondition.split(breakpointValue)[0].trim().matchAll(typeFromName), (m) => m[1]); + return { + name: splitedCondition.split(breakpointValue)[0].trim(), + value: splitedCondition.split(breakpointValue)[1].trim(), + key: key[0], + index, + }; + }), + }); + } + } + return conditions; + }; + + const getConditionContentChildren = (injectDependsTo) => { + const breakpointAndOr = /&&|\|\|/gm; + const breakpointValue = /==/gm; + const typeFromName = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-(.*)-Success/mg; + const conditions = []; + for (const children in injectDependsTo) { + if (Object.hasOwn(injectDependsTo, children)) { + for (const dependency in injectDependsTo[children]) { + if (Object.hasOwn(injectDependsTo[children], dependency)) { + const condition = injectDependsTo[children][dependency][values.inject_id]; + const splittedConditions = condition.split(breakpointAndOr); + conditions.push({ + childrenId: dependency, + mode: condition.includes('||') ? 'or' : 'and', + conditionElement: splittedConditions.map((splitedCondition, index) => { + const key = Array.from(splitedCondition.split(breakpointValue)[0].trim().matchAll(typeFromName), (m) => m[1]); + return { + name: splitedCondition.split(breakpointValue)[0].trim(), + value: splitedCondition.split(breakpointValue)[1].trim(), + key: key[0], + index, + }; + }), + }); + } + } + } + } + return conditions; + }; + + const [parentConditions, setParentConditions] = useState(getConditionContentParent(values.inject_depends_on)); + const [childrenConditions, setChildrenConditions] = useState(getConditionContentChildren(values.inject_depends_to)); + const handleChangeParent = (_event, parent) => { const rx = /\.\$select-parent-(.*)-inject-(.*)/g; const arr = rx.exec(parent.key); @@ -58,10 +125,15 @@ const InjectForm = ({ // be changed when we allow for multiple parents) const anyParent = newParents.find((inject) => inject !== undefined); + const newDependsOn = {}; + newDependsOn[anyParent?.inject.inject_id] = `${anyParent?.inject.inject_id}-Execution-Success == true`; + form.mutators.setValue( 'inject_depends_on', - anyParent?.inject.inject_id || null, + anyParent?.inject.inject_id ? newDependsOn : null, ); + + setParentConditions(getConditionContentParent(newDependsOn)); }; const addParent = () => { @@ -85,7 +157,18 @@ const InjectForm = ({ setChildrens(newChildrens); - form.mutators.setValue('inject_depends_to', newChildrens.map((inject) => inject.inject?.inject_id)); + const newDependsTo = []; + + for (let i = 0; i < newChildrens.length; i += 1) { + const dependsToChildren = {}; + dependsToChildren[newChildrens[i].inject.inject_id] = {}; + dependsToChildren[newChildrens[i].inject.inject_id][values.inject_id] = `${values.inject_id}-Execution-Success == true`; + newDependsTo.push(dependsToChildren); + } + + form.mutators.setValue('inject_depends_to', newDependsTo); + + setChildrenConditions(getConditionContentChildren(newDependsTo)); }; const addChildren = () => { @@ -123,6 +206,298 @@ const InjectForm = ({ form.mutators.setValue('inject_depends_to', newChildrens.map((inject) => inject.inject?.inject_id)); } }; + const addConditionParent = (parent) => { + const { mode } = parentConditions.find((currentCondition) => parent.inject.inject_id === currentCondition.parentId); + + const newDependsOn = values.inject_depends_on; + newDependsOn[parent.inject.inject_id] = `${values.inject_depends_on[parent.inject.inject_id]} ${mode === 'and' ? '&&' : '||'} ${parent.inject.inject_id}-Execution-Success == true`; + form.mutators.setValue( + 'inject_depends_on', + newDependsOn, + ); + + setParentConditions(getConditionContentParent(newDependsOn)); + }; + + const addConditionChildren = (children) => { + const { mode } = childrenConditions.find((currentCondition) => children.inject.inject_id === currentCondition.childrenId); + + let newDependsTo = {}; + newDependsTo = values.inject_depends_to.map((dependsToChildren) => { + if (dependsToChildren[children.inject.inject_id] !== undefined) { + const newValue = {}; + newValue[children.inject.inject_id] = {}; + newValue[children.inject.inject_id][values.inject_id] = `${dependsToChildren[children.inject.inject_id][values.inject_id]} ${mode === 'and' ? '&&' : '||'} ${values.inject_id}-Execution-Success == true`; + return newValue; + } + return dependsToChildren; + }); + form.mutators.setValue( + 'inject_depends_to', + newDependsTo, + ); + setChildrenConditions(getConditionContentChildren(newDependsTo)); + }; + + const conditionsToStringParent = (conditions) => { + const newConditions = {}; + + for (const dependency in conditions) { + if (Object.hasOwn(conditions, dependency)) { + let writtenCondition = ''; + for (const conditionElementIndex in conditions[dependency].conditionElement) { + if (Object.hasOwn(conditions[dependency].conditionElement, conditionElementIndex)) { + writtenCondition += `${conditionElementIndex > 0 ? '&& ' : ''}${conditions[dependency].conditionElement[conditionElementIndex].name} == ${conditions[dependency].conditionElement[conditionElementIndex].value}`; + } + } + newConditions[conditions[dependency].parentId] = writtenCondition; + } + } + return newConditions; + }; + + const conditionsToStringChildren = (conditions) => { + const newConditions = []; + + for (const dependency in conditions) { + if (Object.hasOwn(conditions, dependency)) { + let writtenCondition = ''; + for (const conditionElementIndex in conditions[dependency].conditionElement) { + if (Object.hasOwn(conditions[dependency].conditionElement, conditionElementIndex)) { + writtenCondition += `${conditionElementIndex > 0 ? '&& ' : ''}${conditions[dependency].conditionElement[conditionElementIndex].name} == ${conditions[dependency].conditionElement[conditionElementIndex].value}`; + } + } + const newCondition = {}; + newCondition[conditions[dependency].childrenId] = {}; + newCondition[conditions[dependency].childrenId][values.inject_id] = writtenCondition; + newConditions.push(newCondition); + } + } + return newConditions; + }; + + const changeParentElement = (newElement, conditions, condition, parent) => { + const newConditionElements = conditions.conditionElement.map((newConditionElement) => { + if (newConditionElement.index === condition.index) { + return { + index: condition.index, + key: newElement.key, + name: `${conditions.parentId}-${newElement.key}-Success`, + value: newElement.value === 'Success' ? 'true' : 'false', + }; + } + return newConditionElement; + }); + const newParentConditions = parentConditions.map((parentCondition) => { + if (parentCondition.parentId === parent.inject.inject_id) { + return { + ...parentCondition, + conditionElement: newConditionElements, + }; + } + return parentCondition; + }); + setParentConditions(newParentConditions); + form.mutators.setValue( + 'inject_depends_on', + conditionsToStringParent(newParentConditions), + ); + }; + + const changeChildrenElement = (newElement, conditions, condition, parent) => { + const newConditionElements = conditions.conditionElement.map((newConditionElement) => { + if (newConditionElement.index === condition.index) { + return { + index: condition.index, + key: newElement.key, + name: `${conditions.childrenId}-${newElement.key}-Success`, + value: newElement.value === 'Success' ? 'true' : 'false', + }; + } + return newConditionElement; + }); + const newChildrenConditions = childrenConditions.map((childrenCondition) => { + if (childrenCondition.childrenId === parent.inject.inject_id) { + return { + ...childrenCondition, + conditionElement: newConditionElements, + }; + } + return childrenCondition; + }); + setChildrenConditions(newChildrenConditions); + form.mutators.setValue( + 'inject_depends_to', + conditionsToStringChildren(newChildrenConditions), + ); + }; + + const changeModeParent = (conditions, condition) => { + const newConditionElements = conditions.map((currentCondition) => { + if (currentCondition.parentId === condition.parentId) { + return { + ...currentCondition, + mode: currentCondition.mode === 'and' ? 'or' : 'and', + }; + } + return currentCondition; + }); + setParentConditions(newConditionElements); + }; + + const changeModeChildren = (conditions, condition) => { + const newConditionElements = conditions.map((currentCondition) => { + if (currentCondition.childrenId === condition.childrenId) { + return { + ...currentCondition, + mode: currentCondition.mode === 'and' ? 'or' : 'and', + }; + } + return currentCondition; + }); + setChildrenConditions(newConditionElements); + }; + + const deleteConditionParent = (conditions, condition) => { + const newConditionElements = parentConditions.map((currentCondition) => { + if (currentCondition.parentId === conditions.parentId) { + return { + ...currentCondition, + conditionElement: currentCondition.conditionElement.filter((element) => element.index !== condition.index), + }; + } + return currentCondition; + }); + setParentConditions(newConditionElements); + form.mutators.setValue( + 'inject_depends_on', + conditionsToStringParent(newConditionElements), + ); + }; + + const deleteConditionChildren = (conditions, condition) => { + const newConditionElements = childrenConditions.map((currentCondition) => { + if (currentCondition.childrenId === conditions.childrenId) { + return { + ...currentCondition, + conditionElement: currentCondition.conditionElement.filter((element) => element.index !== condition.index), + }; + } + return currentCondition; + }); + setChildrenConditions(newConditionElements); + form.mutators.setValue( + 'inject_depends_to', + conditionsToStringChildren(newConditionElements), + ); + }; + + const capitalize = (text) => { + return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); + }; + + const getAvailableExpectations = (inject) => { + if (inject.inject_content !== null && inject.inject_content !== undefined) { + const expectations = inject.inject_content.expectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); + return ['Execution', ...expectations]; + } if (inject.inject_injector_contract !== undefined + && inject.inject_injector_contract.convertedContent.fields.find((field) => field.key === 'expectations')) { + const expectations = inject.inject_injector_contract.convertedContent.fields + .find((field) => field.key === 'expectations') + .predefinedExpectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); + return ['Execution', ...expectations]; + } + return ['Execution']; + }; + + const getClickableParentChip = (parent) => { + const parentChip = parentConditions.find((parentCondition) => parent.inject !== undefined && parentCondition.parentId === parent.inject.inject_id); + if (parentChip === undefined) return (<>); + return parentChip.conditionElement.map((condition, conditionIndex) => { + const conditions = parentConditions + .find((parentCondition) => parent.inject !== undefined && parentCondition.parentId === parent.inject.inject_id); + if (conditionIndex < conditions.conditionElement.length - 1) { + return (<> 1 ? () => { deleteConditionParent(conditions, condition); } : undefined + } + onChange={(newElement) => { + changeParentElement(newElement, conditions, condition, parent); + }} + /> { changeModeParent(parentConditions, conditions); }} + />); + } + return ( 1 ? () => { deleteConditionParent(conditions, condition); } : undefined + } + onChange={(newElement) => { + changeParentElement(newElement, conditions, condition, parent); + }} + />); + }); + }; + + const getClickableChildrenChip = (children) => { + const childrenChip = childrenConditions.find((childrenCondition) => children.inject !== undefined && childrenCondition.childrenId === children.inject.inject_id); + if (childrenChip === undefined) return (<>); + return childrenChip + .conditionElement.map((condition, conditionIndex) => { + const conditions = childrenConditions + .find((childrenCondition) => childrenCondition.childrenId === children.inject.inject_id); + if (conditionIndex < conditions.conditionElement.length - 1) { + return (<> currentInject.inject_id === values.inject_id))} + availableOperators={['is']} + availableValues={['Success', 'Fail']} + onDelete={ + conditions.conditionElement.length > 1 ? () => { deleteConditionChildren(conditions, condition); } : undefined + } + onChange={(newElement) => { + changeChildrenElement(newElement, conditions, condition, children); + }} + /> { changeModeChildren(childrenConditions, conditions); }} + />); + } + return ( currentInject.inject_id === values.inject_id))} + availableOperators={['is']} + availableValues={['Success', 'Fail']} + onDelete={ + conditions.conditionElement.length > 1 ? () => { deleteConditionChildren(conditions, condition); } : undefined + } + onChange={(newElement) => { + changeChildrenElement(newElement, conditions, condition, children); + }} + />); + }); + }; return ( <> @@ -185,15 +560,31 @@ const InjectForm = ({ - {t('Condition')} - + {getClickableParentChip(parent)} + + @@ -259,15 +650,32 @@ const InjectForm = ({ - {t('Condition')} - + {getClickableChildrenChip(children)} + + diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js index ecf2905511..dab8c29243 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js @@ -18,6 +18,7 @@ import { isEmptyField } from '../../../../utils/utils'; import { tagOptions } from '../../../../utils/Option'; import { splitDuration } from '../../../../utils/Time'; import PlatformIcon from '../../../../components/PlatformIcon'; +import { fromInjectDependencyToInputDependency } from './chaining/ChainingUtils'; const useStyles = makeStyles((theme) => ({ details: { @@ -197,6 +198,7 @@ const UpdateInjectDetails = ({ + data.inject_depends_duration_hours * 3600 + data.inject_depends_duration_minutes * 60; const inject_tags = !R.isEmpty(data.inject_tags) ? R.pluck('id', data.inject_tags) : []; + console.log(data.inject_depends_on); const values = { inject_title: data.inject_title, inject_injector_contract: contractContent.contract_id, @@ -209,7 +211,7 @@ const UpdateInjectDetails = ({ inject_asset_groups: assetGroupIds, inject_documents: documents, inject_depends_duration, - inject_depends_on: data.inject_depends_on, + inject_depends_on: fromInjectDependencyToInputDependency(data.inject_depends_on), }; await onUpdateInject(values); } diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js index 43ac86e834..576869caa9 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js @@ -33,11 +33,24 @@ const UpdateInjectLogicalChains = ({ const { t, tPick } = useFormatter(); const classes = useStyles(); + const injectDependsOn = {}; + if (inject.inject_depends_on !== null) { + for (let i = 0; i < inject.inject_depends_on.length; i += 1) { + injectDependsOn[inject.inject_depends_on[i].dependency_relationship.inject_parent_id] = inject.inject_depends_on[i].dependency_condition; + } + } + console.log(injectDependsOn); + const initialValues = { ...inject, inject_depends_to: injects - .filter((currentInject) => currentInject.inject_depends_on === inject.inject_id) - .map((currentInject) => currentInject.inject_id), + .filter((currentInject) => currentInject.inject_depends_on !== null && currentInject.inject_depends_on[inject.inject_id] !== undefined) + .map((currentInject) => { + const inject_depends = {}; + inject_depends[currentInject.inject_id] = currentInject.inject_depends_on; + return inject_depends; + }), + inject_depends_on: inject.inject_depends_on !== null ? injectDependsOn : null, }; const onSubmit = async (data) => { @@ -50,10 +63,11 @@ const UpdateInjectLogicalChains = ({ const injectsToUpdate = []; - const childrenIds = data.inject_depends_to; - + console.log(data.inject_depends_to); + const childrenIds = data.inject_depends_to.flatMap((childrenInject) => Object.keys(childrenInject)); const injectsWithoutDependencies = injects - .filter((currentInject) => currentInject.inject_depends_on === data.inject_id + .filter((currentInject) => currentInject.inject_depends_on !== null + && currentInject.inject_depends_on[data.inject_id] !== undefined && !childrenIds.includes(currentInject.inject_id)) .map((currentInject) => { return { @@ -69,11 +83,18 @@ const UpdateInjectLogicalChains = ({ childrenIds.forEach((childrenId) => { const children = injects.find((currentInject) => currentInject.inject_id === childrenId); if (children !== undefined) { + const injectDependsOnUpdate = {}; + for (let i = 0; i < data.inject_depends_to.length; i += 1) { + if (data.inject_depends_to[i][childrenId] !== undefined) { + injectDependsOnUpdate[inject.inject_id] = data.inject_depends_to[i][childrenId][inject.inject_id]; + } + } + const injectChildrenUpdate = { ...children, inject_id: children.inject_id, inject_injector_contract: children.inject_injector_contract.injector_contract_id, - inject_depends_on: inject.inject_id, + inject_depends_on: injectDependsOnUpdate, }; injectsToUpdate.push(injectChildrenUpdate); } diff --git a/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx b/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx new file mode 100644 index 0000000000..2cfe5e2f0a --- /dev/null +++ b/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx @@ -0,0 +1,13 @@ +import type { InjectDependency } from '../../../../../utils/api-types'; + +export const fromInjectDependencyToInputDependency = (dependencies: InjectDependency[]) => { + const result : Record = {}; + for (let i = 0; i < dependencies.length; i += 1) { + if (dependencies[i].dependency_relationship?.inject_parent_id !== undefined + && dependencies[i].dependency_condition !== undefined) { + const key = dependencies[i].dependency_relationship!.inject_parent_id; + result[key] = dependencies[i].dependency_condition!; + } + } + return dependencies.length > 0 ? result : null; +}; diff --git a/openbas-front/src/components/ChainedTimeline.tsx b/openbas-front/src/components/ChainedTimeline.tsx index d159b3a7ed..d531160389 100644 --- a/openbas-front/src/components/ChainedTimeline.tsx +++ b/openbas-front/src/components/ChainedTimeline.tsx @@ -177,18 +177,25 @@ const ChainedTimelineFlow: FunctionComponent = ({ }; const updateEdges = () => { - const newEdges = injects.filter((inject) => inject.inject_depends_on != null).map((inject) => { - return ({ - id: `${inject.inject_id}->${inject.inject_depends_on}`, - target: `${inject.inject_id}`, - targetHandle: `target-${inject.inject_id}`, - source: `${inject.inject_depends_on}`, - sourceHandle: `source-${inject.inject_depends_on}`, - label: '', - labelShowBg: false, - labelStyle: { fill: theme.palette.text?.primary, fontSize: 9 }, + const newEdges = injects.filter((inject) => inject.inject_depends_on != null && inject.inject_depends_on !== undefined) + .flatMap((inject) => { + const results = []; + if (inject.inject_depends_on !== undefined) { + for (const key of Object.keys(inject.inject_depends_on)) { + results.push({ + id: `${inject.inject_id}->${key}`, + target: `${inject.inject_id}`, + targetHandle: `target-${inject.inject_id}`, + source: `${key}`, + sourceHandle: `source-${key}`, + label: '', + labelShowBg: false, + labelStyle: { fill: theme.palette.text?.primary, fontSize: 9 }, + }); + } + } + return results; }); - }); setEdges(newEdges); }; @@ -316,11 +323,13 @@ const ChainedTimelineFlow: FunctionComponent = ({ const inject = injects.find((currentInject) => currentInject.inject_id === connection.target); const injectParent = injects.find((currentInject) => currentInject.inject_id === connection.source); if (inject !== undefined && injectParent !== undefined && inject.inject_depends_duration > injectParent.inject_depends_duration) { + const newDependsOn = {}; + newDependsOn[injectParent?.inject_id] = `${injectParent?.inject_id}-Execution-Success == true`; const injectToUpdate = { ...injectsMap[inject.inject_id], inject_injector_contract: inject.inject_injector_contract.injector_contract_id, inject_id: inject.inject_id, - inject_depends_on: injectParent?.inject_id, + inject_depends_on: newDependsOn, }; onUpdateInject([injectToUpdate]); } @@ -335,8 +344,10 @@ const ChainedTimelineFlow: FunctionComponent = ({ setDraggingOnGoing(true); const { position } = node; const { data } = node; - const dependsOn = nodes.find((currentNode) => (currentNode.id === data.inject?.inject_depends_on)); - const dependsTo = nodes.filter((currentNode) => (currentNode.data.inject?.inject_depends_on === node.id)) + const dependsOn = nodes.find((currentNode) => (data.inject?.inject_depends_on !== null + && data.inject?.inject_depends_on![currentNode.id])); + const dependsTo = nodes + .filter((currentNode) => (currentNode.data.inject?.inject_depends_on !== undefined && currentNode.data.inject?.inject_depends_on !== null && currentNode.data.inject?.inject_depends_on[node.id] !== undefined)) .sort((a, b) => a.data.inject!.inject_depends_duration - b.data.inject!.inject_depends_duration)[0]; const aSecond = gapSize / (minutesPerGapAllowed[minutesPerGapIndex] * 60); if (dependsOn?.position && position.x <= dependsOn?.position.x) { @@ -462,11 +473,13 @@ const ChainedTimelineFlow: FunctionComponent = ({ inject_depends_on: undefined, }; updates.push(injectToRemoveEdge); + const newDependsOn = {}; + newDependsOn[edge.source] = `${edge.source}-Execution-Success == true`; const injectToUpdateEdge = { ...injectsMap[injectToUpdate.inject_id], inject_injector_contract: injectToUpdate.inject_injector_contract.injector_contract_id, inject_id: injectToUpdate.inject_id, - inject_depends_on: edge.source, + inject_depends_on: newDependsOn, }; updates.push(injectToUpdateEdge); onUpdateInject(updates); @@ -475,6 +488,8 @@ const ChainedTimelineFlow: FunctionComponent = ({ const inject = injects.find((currentInject) => currentInject.inject_id === edge.target); const parent = injects.find((currentInject) => currentInject.inject_id === connectionState.toNode?.id); if (inject !== undefined && parent !== undefined && parent.inject_depends_duration < inject.inject_depends_duration) { + const newDependsOn = {}; + newDependsOn[connectionState.toNode?.id] = `${connectionState.toNode?.id}-Execution-Success == true`; const injectToUpdate = { ...injectsMap[inject.inject_id], inject_injector_contract: inject.inject_injector_contract.injector_contract_id, diff --git a/openbas-front/src/components/common/chips/ChipUtils.tsx b/openbas-front/src/components/common/chips/ChipUtils.tsx new file mode 100644 index 0000000000..c46c97b9a7 --- /dev/null +++ b/openbas-front/src/components/common/chips/ChipUtils.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +// -- OPERATOR -- + +export const convertOperatorToIcon = (t: (text: string) => string, operator?: string) => { + switch (operator) { + case 'is': + return <> {t('is')}; + default: + return null; + } +}; diff --git a/openbas-front/src/components/common/chips/ClickableChip.tsx b/openbas-front/src/components/common/chips/ClickableChip.tsx new file mode 100644 index 0000000000..4169bae745 --- /dev/null +++ b/openbas-front/src/components/common/chips/ClickableChip.tsx @@ -0,0 +1,181 @@ +import React, { FunctionComponent, useRef, useState } from 'react'; +import { Box, Chip, SelectChangeEvent, Tooltip } from '@mui/material'; +import { makeStyles } from '@mui/styles'; +import classNames from 'classnames'; +import type { Theme } from '../../Theme'; +import { useFormatter } from '../../i18n'; +import { convertOperatorToIcon } from './ChipUtils'; +import ClickableChipPopover from './ClickableChipPopover'; + +const useStyles = makeStyles((theme: Theme) => ({ + mode: { + display: 'inline-block', + height: '100%', + backgroundColor: theme.palette.action?.selected, + margin: '0 4px', + padding: '0 4px', + }, + modeTooltip: { + margin: '0 4px', + }, + container: { + gap: '4px', + display: 'flex', + overflow: 'hidden', + maxWidth: '400px', + alignItems: 'center', + lineHeight: '32px', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }, + interactive: { + cursor: 'pointer', + '&:hover': { + textDecorationLine: 'underline', + }, + }, +})); + +interface Element { + key: string; + operator?: string; + value?: string; +} + +interface Props { + onChange: (newElement: Element) => void; + pristine: boolean; + selectedElement: Element, + availableKeys: string[], + availableOperators: string[], + availableValues: string[], + onDelete?: () => void, +} + +const ClickableChip: FunctionComponent = ({ + onChange, + pristine, + selectedElement, + availableKeys, + availableOperators, + availableValues, + onDelete, +}) => { + // Standard hooks + const { t } = useFormatter(); + const classes = useStyles(); + + const chipRef = useRef(null); + const [open, setOpen] = useState(!pristine); + const [availableOptions, setAvailableOptions] = useState([]); + const [selectedValue, setSelectedValue] = useState(); + const [propertyToChange, setPropertyToChange] = useState(''); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + const handleRemoveFilter = () => { + if (onDelete) onDelete(); + }; + + const handleChange = (event: SelectChangeEvent) => { + const newValue = selectedElement; + switch (propertyToChange) { + case 'key': { + newValue.key = event.target.value; + break; + } + case 'operator': { + newValue.operator = event.target.value; + break; + } + case 'value': { + newValue.value = event.target.value; + break; + } + default: + break; + } + onChange(newValue); + setOpen(false); + }; + + const handleClickOpen = (options: string[], property: string, optionValue?: string) => { + setAvailableOptions(options); + if (optionValue) setSelectedValue(optionValue); + if (options.length > 1) handleOpen(); + setPropertyToChange(property); + }; + + const toValues = (opts: string[] | undefined, isTooltip: boolean) => { + if (opts !== undefined) { + return opts.map((o, idx) => { + let or = <>; + if (idx > 0) { + or =
+ {t('OR')} +
; + } + return (<>{or} {o}); + }); + } + return ( {t('undefined')}); + }; + + const filterValues = (isTooltip: boolean) => { + return ( + + 1 ? classes.interactive : undefined} + onClick={() => handleClickOpen(availableKeys, 'key', selectedElement.key)} + > + {t(selectedElement.key)} + + 1 ? classes.interactive : undefined} + onClick={() => handleClickOpen(availableOperators, 'operator', selectedElement.operator)} + > + {convertOperatorToIcon(t, selectedElement.operator)} + + 1 ? classes.interactive : undefined} + onClick={() => handleClickOpen(availableValues, 'value', selectedElement.value)} + > + {toValues(selectedElement.value ? [selectedElement.value] : [], isTooltip)} + + + ); + }; + + const chipVariant = 'filled'; + + return ( + <> + + + + {chipRef?.current + && + } + + ); +}; +export default ClickableChip; diff --git a/openbas-front/src/components/common/chips/ClickableChipPopover.tsx b/openbas-front/src/components/common/chips/ClickableChipPopover.tsx new file mode 100644 index 0000000000..9a87fb82cf --- /dev/null +++ b/openbas-front/src/components/common/chips/ClickableChipPopover.tsx @@ -0,0 +1,73 @@ +import React, { FunctionComponent } from 'react'; +import { MenuItem, Popover, Select, SelectChangeEvent } from '@mui/material'; +import { useFormatter } from '../../i18n'; + +interface Props { + handleChangeValue: (event: SelectChangeEvent) => void; + open: boolean; + onClose: () => void; + anchorEl?: HTMLElement; + availableValues: string[]; + element?: string, +} + +const ClickableChipPopover: FunctionComponent = ({ + handleChangeValue, + open, + onClose, + anchorEl, + availableValues, + element, +}) => { + // Standard hooks + const { t } = useFormatter(); + + console.log(availableValues); + + const displayOperatorAndFilter = () => { + // Specific field + + return ( + <> + + + ); + }; + + return ( + +
+ {displayOperatorAndFilter()} +
+
+ ); +}; +export default ClickableChipPopover; diff --git a/openbas-front/src/components/common/chips/ClickableModeChip.tsx b/openbas-front/src/components/common/chips/ClickableModeChip.tsx new file mode 100644 index 0000000000..b726b8c074 --- /dev/null +++ b/openbas-front/src/components/common/chips/ClickableModeChip.tsx @@ -0,0 +1,55 @@ +import React, { FunctionComponent } from 'react'; +import { makeStyles } from '@mui/styles'; +import classNames from 'classnames'; +import { useFormatter } from '../../i18n'; +import type { Theme } from '../../Theme'; + +const useStyles = makeStyles((theme: Theme) => ({ + mode: { + borderRadius: 4, + fontFamily: 'Consolas, monaco, monospace', + backgroundColor: theme.palette.action?.selected, + padding: '0 8px', + display: 'flex', + alignItems: 'center', + }, + hasClickEvent: { + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.action?.disabled, + textDecorationLine: 'underline', + }, + }, +})); + +interface Props { + onClick?: () => void; + mode?: 'and' | 'or'; +} + +const ClickableModeChip: FunctionComponent = ({ + onClick, + mode, +}) => { + // Standard hooks + const classes = useStyles(); + const { t } = useFormatter(); + + if (!mode) { + return <>; + } + + return ( +
+ {t(mode.toUpperCase())} +
+ ); +}; + +export default ClickableModeChip; diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 2f7824239f..4be6b8839e 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -1073,7 +1073,7 @@ export interface Inject { * @min 0 */ inject_depends_duration: number; - inject_depends_on?: Inject; + inject_depends_on?: InjectDependency[]; inject_description?: string; inject_documents?: InjectDocument[]; inject_enabled?: boolean; @@ -1104,6 +1104,20 @@ export interface Inject { listened?: boolean; } +export interface InjectDependency { + dependency_condition?: string; + /** @format date-time */ + dependency_created_at?: string; + dependency_relationship?: InjectDependencyId; + /** @format date-time */ + dependency_updated_at?: string; +} + +export interface InjectDependencyId { + inject_children_id?: Inject; + inject_parent_id?: Inject; +} + export interface InjectDocument { document_attached?: boolean; document_id?: Document; @@ -1221,7 +1235,7 @@ export interface InjectInput { inject_country?: string; /** @format int64 */ inject_depends_duration?: number; - inject_depends_on?: string; + inject_depends_on?: Record; inject_description?: string; inject_documents?: InjectDocumentInput[]; inject_injector_contract?: string; @@ -1239,7 +1253,7 @@ export interface InjectOutput { * @min 0 */ inject_depends_duration: number; - inject_depends_on?: string; + inject_depends_on?: Record; inject_enabled?: boolean; inject_exercise?: string; inject_id: string; diff --git a/openbas-model/src/main/java/io/openbas/database/model/Inject.java b/openbas-model/src/main/java/io/openbas/database/model/Inject.java index e8b76fa11f..bc3838f6d7 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Inject.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Inject.java @@ -123,11 +123,9 @@ public class Inject implements Base, Injection { private Scenario scenario; @Getter - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "inject_depends_from_another") - @JsonSerialize(using = MonoIdDeserializer.class) + @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) @JsonProperty("inject_depends_on") - private Inject dependsOn; + private List dependsOn; @Getter @Column(name = "inject_depends_duration") @@ -279,7 +277,7 @@ public boolean isReady() { @JsonIgnore public Instant computeInjectDate(Instant source, int speed) { - return InjectModelHelper.computeInjectDate(source, speed, getDependsOn(), getDependsDuration(), getExercise()); + return InjectModelHelper.computeInjectDate(source, speed, getDependsDuration(), getExercise()); } @JsonProperty("inject_date") @@ -291,7 +289,7 @@ public Optional getDate() { return Optional.of(now().minusSeconds(60)); } } - return InjectModelHelper.getDate(getExercise(), getScenario(), getDependsOn(), getDependsDuration()); + return InjectModelHelper.getDate(getExercise(), getScenario(), getDependsDuration()); } @JsonIgnore diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependency.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependency.java new file mode 100644 index 0000000000..fe1c0ba6e7 --- /dev/null +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependency.java @@ -0,0 +1,52 @@ +package io.openbas.database.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.openbas.helper.MonoIdDeserializer; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; + +import java.time.Instant; +import java.util.Objects; + +@Setter +@Getter +@Entity +@Table(name = "injects_dependencies") +public class InjectDependency { + + @EmbeddedId + @JsonProperty("dependency_relationship") + private InjectDependencyId compositeId = new InjectDependencyId(); + + @Column(name = "dependency_condition") + @JsonProperty("dependency_condition") + private String condition; + + @CreationTimestamp + @Column(name = "dependency_created_at") + @JsonProperty("dependency_created_at") + private Instant creationDate; + + @UpdateTimestamp + @Column(name = "dependency_updated_at") + @JsonProperty("dependency_updated_at") + private Instant updateDate; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InjectDependency that = (InjectDependency) o; + return compositeId.equals(that.compositeId); + } + + @Override + public int hashCode() { + return Objects.hash(compositeId); + } +} diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java new file mode 100644 index 0000000000..907b9513d7 --- /dev/null +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java @@ -0,0 +1,50 @@ +package io.openbas.database.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.openbas.helper.MonoIdDeserializer; +import jakarta.persistence.Embeddable; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +@Getter +@Setter +@NoArgsConstructor +public class InjectDependencyId implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @ManyToOne + @JsonProperty("inject_parent_id") + @JoinColumn(referencedColumnName="inject_id", name="inject_parent_id") + @JsonSerialize(using = MonoIdDeserializer.class) + private Inject injectParentId; + + @ManyToOne + @JsonProperty("inject_children_id") + @JoinColumn(referencedColumnName="inject_id", name="inject_children_id") + @JsonSerialize(using = MonoIdDeserializer.class) + private Inject injectChildrenId; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InjectDependencyId that = (InjectDependencyId) o; + return injectParentId.equals(that.injectParentId) && injectChildrenId.equals(that.injectChildrenId); + } + + @Override + public int hashCode() { + return Objects.hash(injectParentId, injectChildrenId); + } +} diff --git a/openbas-model/src/main/java/io/openbas/database/repository/InjectDependenciesRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/InjectDependenciesRepository.java new file mode 100644 index 0000000000..2304a35713 --- /dev/null +++ b/openbas-model/src/main/java/io/openbas/database/repository/InjectDependenciesRepository.java @@ -0,0 +1,37 @@ +package io.openbas.database.repository; + +import io.openbas.database.model.InjectDependency; +import io.openbas.database.model.InjectDependencyId; +import io.openbas.database.model.Injector; +import jakarta.validation.constraints.NotNull; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface InjectDependenciesRepository extends CrudRepository, JpaSpecificationExecutor { + + @Query(value = "SELECT " + + "inject_parent_id, " + + "inject_children_id, " + + "dependency_condition, " + + "dependency_created_at, " + + "dependency_updated_at " + + "FROM injects_dependencies " + + "WHERE inject_children_id IN :childrens", nativeQuery = true) + List findParents(@NotNull List childrens); + + @Query(value = "SELECT " + + "inject_parent_id, " + + "inject_children_id, " + + "dependency_condition, " + + "dependency_created_at, " + + "dependency_updated_at " + + "FROM injects_dependencies " + + "WHERE inject_parent_id IN :parents", nativeQuery = true) + List findChildrens(@NotNull List parents); +} diff --git a/openbas-model/src/main/java/io/openbas/helper/InjectModelHelper.java b/openbas-model/src/main/java/io/openbas/helper/InjectModelHelper.java index fb39b73f27..c0ef540741 100644 --- a/openbas-model/src/main/java/io/openbas/helper/InjectModelHelper.java +++ b/openbas-model/src/main/java/io/openbas/helper/InjectModelHelper.java @@ -64,15 +64,11 @@ public static boolean isReady( public static Instant computeInjectDate( Instant source, int speed, - Inject dependsOn, Long dependsDuration, Exercise exercise) { // Compute origin execution date - Optional dependsOnInject = ofNullable(dependsOn); long duration = ofNullable(dependsDuration).orElse(0L) / speed; - Instant dependingStart = dependsOnInject - .map(inject -> inject.computeInjectDate(source, speed)) - .orElse(source); + Instant dependingStart = source; Instant standardExecutionDate = dependingStart.plusSeconds(duration); // Compute execution dates with previous terminated pauses long previousPauseDelay = 0L; @@ -99,7 +95,6 @@ public static Instant computeInjectDate( public static Optional getDate( Exercise exercise, Scenario scenario, - Inject dependsOn, Long dependsDuration ) { if (exercise == null && scenario == null) { @@ -116,7 +111,7 @@ public static Optional getDate( } return exercise .getStart() - .map(source -> computeInjectDate(source, SPEED_STANDARD, dependsOn, dependsDuration, exercise)); + .map(source -> computeInjectDate(source, SPEED_STANDARD, dependsDuration, exercise)); } return Optional.ofNullable(LocalDateTime.now().toInstant(ZoneOffset.UTC)); } From 15b0fe21e827635322b50a1688a1633fbed421b6 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Tue, 1 Oct 2024 09:35:45 +0200 Subject: [PATCH 02/34] [frontend/backend] Chaining injects conditionally --- .../src/admin/components/common/injects/Injects.tsx | 1 + .../common/injects/chaining/ChainingUtils.tsx | 8 ++++++-- openbas-front/src/components/ChainedTimeline.tsx | 12 +++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index 31618b2f48..42b59945d9 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -253,6 +253,7 @@ const Injects: FunctionComponent = ({ data.forEach((inject) => { promises.push(injectContext.onUpdateInject(inject.inject_id, inject).then((result: { result: string, entities: { injects: Record } }) => { if (result.entities) { + onUpdate(result); return result.entities.injects[result.result]; } return undefined; diff --git a/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx b/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx index 2cfe5e2f0a..9c6d5f38e0 100644 --- a/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx +++ b/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx @@ -1,13 +1,17 @@ import type { InjectDependency } from '../../../../../utils/api-types'; -export const fromInjectDependencyToInputDependency = (dependencies: InjectDependency[]) => { +const fromInjectDependencyToInputDependency = (dependencies: InjectDependency[]) => { const result : Record = {}; for (let i = 0; i < dependencies.length; i += 1) { if (dependencies[i].dependency_relationship?.inject_parent_id !== undefined && dependencies[i].dependency_condition !== undefined) { const key = dependencies[i].dependency_relationship!.inject_parent_id; - result[key] = dependencies[i].dependency_condition!; + if (key !== undefined) { + result[key] = dependencies[i].dependency_condition!; + } } } return dependencies.length > 0 ? result : null; }; + +export default fromInjectDependencyToInputDependency; diff --git a/openbas-front/src/components/ChainedTimeline.tsx b/openbas-front/src/components/ChainedTimeline.tsx index d531160389..160bc62e83 100644 --- a/openbas-front/src/components/ChainedTimeline.tsx +++ b/openbas-front/src/components/ChainedTimeline.tsx @@ -323,7 +323,7 @@ const ChainedTimelineFlow: FunctionComponent = ({ const inject = injects.find((currentInject) => currentInject.inject_id === connection.target); const injectParent = injects.find((currentInject) => currentInject.inject_id === connection.source); if (inject !== undefined && injectParent !== undefined && inject.inject_depends_duration > injectParent.inject_depends_duration) { - const newDependsOn = {}; + const newDependsOn: Record = {}; newDependsOn[injectParent?.inject_id] = `${injectParent?.inject_id}-Execution-Success == true`; const injectToUpdate = { ...injectsMap[inject.inject_id], @@ -347,7 +347,9 @@ const ChainedTimelineFlow: FunctionComponent = ({ const dependsOn = nodes.find((currentNode) => (data.inject?.inject_depends_on !== null && data.inject?.inject_depends_on![currentNode.id])); const dependsTo = nodes - .filter((currentNode) => (currentNode.data.inject?.inject_depends_on !== undefined && currentNode.data.inject?.inject_depends_on !== null && currentNode.data.inject?.inject_depends_on[node.id] !== undefined)) + .filter((currentNode) => (currentNode.data.inject?.inject_depends_on !== undefined + && currentNode.data.inject?.inject_depends_on !== null + && currentNode.data.inject?.inject_depends_on[node.id] !== undefined)) .sort((a, b) => a.data.inject!.inject_depends_duration - b.data.inject!.inject_depends_duration)[0]; const aSecond = gapSize / (minutesPerGapAllowed[minutesPerGapIndex] * 60); if (dependsOn?.position && position.x <= dependsOn?.position.x) { @@ -473,7 +475,7 @@ const ChainedTimelineFlow: FunctionComponent = ({ inject_depends_on: undefined, }; updates.push(injectToRemoveEdge); - const newDependsOn = {}; + const newDependsOn: Record = {}; newDependsOn[edge.source] = `${edge.source}-Execution-Success == true`; const injectToUpdateEdge = { ...injectsMap[injectToUpdate.inject_id], @@ -488,8 +490,8 @@ const ChainedTimelineFlow: FunctionComponent = ({ const inject = injects.find((currentInject) => currentInject.inject_id === edge.target); const parent = injects.find((currentInject) => currentInject.inject_id === connectionState.toNode?.id); if (inject !== undefined && parent !== undefined && parent.inject_depends_duration < inject.inject_depends_duration) { - const newDependsOn = {}; - newDependsOn[connectionState.toNode?.id] = `${connectionState.toNode?.id}-Execution-Success == true`; + const newDependsOn: Record = {}; + newDependsOn[connectionState.toNode!.id] = `${connectionState.toNode?.id}-Execution-Success == true`; const injectToUpdate = { ...injectsMap[inject.inject_id], inject_injector_contract: inject.inject_injector_contract.injector_contract_id, From 68ad3c02edf5b3ca289d58ccb44ab5fa7fbfa88b Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 2 Oct 2024 14:52:12 +0200 Subject: [PATCH 03/34] [frontend/backend] Chaining injects conditionally --- .../src/admin/components/common/injects/UpdateInjectDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js index dab8c29243..7875c6ee70 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js @@ -18,7 +18,7 @@ import { isEmptyField } from '../../../../utils/utils'; import { tagOptions } from '../../../../utils/Option'; import { splitDuration } from '../../../../utils/Time'; import PlatformIcon from '../../../../components/PlatformIcon'; -import { fromInjectDependencyToInputDependency } from './chaining/ChainingUtils'; +import fromInjectDependencyToInputDependency from './chaining/ChainingUtils'; const useStyles = makeStyles((theme) => ({ details: { From 94d37ca1f4a6b0fabe8839d00c657b91da263fb7 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 2 Oct 2024 15:03:30 +0200 Subject: [PATCH 04/34] [frontend/backend] Chaining injects conditionally --- .../admin/components/common/injects/chaining/ChainingUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx b/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx index 9c6d5f38e0..c51683d916 100644 --- a/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx +++ b/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx @@ -7,7 +7,7 @@ const fromInjectDependencyToInputDependency = (dependencies: InjectDependency[]) && dependencies[i].dependency_condition !== undefined) { const key = dependencies[i].dependency_relationship!.inject_parent_id; if (key !== undefined) { - result[key] = dependencies[i].dependency_condition!; + result[key as unknown as string] = dependencies[i].dependency_condition!; } } } From 2b31dfd1c9aa0d80251497d947f538b0397545e7 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 2 Oct 2024 16:15:33 +0200 Subject: [PATCH 05/34] [frontend/backend] Chaining injects conditionally --- .../admin/components/common/injects/UpdateInjectDetails.js | 1 - .../components/common/injects/UpdateInjectLogicalChains.js | 2 -- openbas-front/src/components/common/chips/ChipUtils.tsx | 4 +++- openbas-front/src/components/common/chips/ClickableChip.tsx | 2 +- .../src/components/common/chips/ClickableChipPopover.tsx | 4 ---- 5 files changed, 4 insertions(+), 9 deletions(-) diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js index 7875c6ee70..2c18d11d0b 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js @@ -198,7 +198,6 @@ const UpdateInjectDetails = ({ + data.inject_depends_duration_hours * 3600 + data.inject_depends_duration_minutes * 60; const inject_tags = !R.isEmpty(data.inject_tags) ? R.pluck('id', data.inject_tags) : []; - console.log(data.inject_depends_on); const values = { inject_title: data.inject_title, inject_injector_contract: contractContent.contract_id, diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js index 576869caa9..576847181f 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js @@ -39,7 +39,6 @@ const UpdateInjectLogicalChains = ({ injectDependsOn[inject.inject_depends_on[i].dependency_relationship.inject_parent_id] = inject.inject_depends_on[i].dependency_condition; } } - console.log(injectDependsOn); const initialValues = { ...inject, @@ -63,7 +62,6 @@ const UpdateInjectLogicalChains = ({ const injectsToUpdate = []; - console.log(data.inject_depends_to); const childrenIds = data.inject_depends_to.flatMap((childrenInject) => Object.keys(childrenInject)); const injectsWithoutDependencies = injects .filter((currentInject) => currentInject.inject_depends_on !== null diff --git a/openbas-front/src/components/common/chips/ChipUtils.tsx b/openbas-front/src/components/common/chips/ChipUtils.tsx index c46c97b9a7..67dc6a6970 100644 --- a/openbas-front/src/components/common/chips/ChipUtils.tsx +++ b/openbas-front/src/components/common/chips/ChipUtils.tsx @@ -2,7 +2,7 @@ import React from 'react'; // -- OPERATOR -- -export const convertOperatorToIcon = (t: (text: string) => string, operator?: string) => { +const convertOperatorToIcon = (t: (text: string) => string, operator?: string) => { switch (operator) { case 'is': return <> {t('is')}; @@ -10,3 +10,5 @@ export const convertOperatorToIcon = (t: (text: string) => string, operator?: st return null; } }; + +export default convertOperatorToIcon; diff --git a/openbas-front/src/components/common/chips/ClickableChip.tsx b/openbas-front/src/components/common/chips/ClickableChip.tsx index 4169bae745..a8ecac70bb 100644 --- a/openbas-front/src/components/common/chips/ClickableChip.tsx +++ b/openbas-front/src/components/common/chips/ClickableChip.tsx @@ -4,7 +4,7 @@ import { makeStyles } from '@mui/styles'; import classNames from 'classnames'; import type { Theme } from '../../Theme'; import { useFormatter } from '../../i18n'; -import { convertOperatorToIcon } from './ChipUtils'; +import convertOperatorToIcon from './ChipUtils'; import ClickableChipPopover from './ClickableChipPopover'; const useStyles = makeStyles((theme: Theme) => ({ diff --git a/openbas-front/src/components/common/chips/ClickableChipPopover.tsx b/openbas-front/src/components/common/chips/ClickableChipPopover.tsx index 9a87fb82cf..8ac0579da8 100644 --- a/openbas-front/src/components/common/chips/ClickableChipPopover.tsx +++ b/openbas-front/src/components/common/chips/ClickableChipPopover.tsx @@ -1,6 +1,5 @@ import React, { FunctionComponent } from 'react'; import { MenuItem, Popover, Select, SelectChangeEvent } from '@mui/material'; -import { useFormatter } from '../../i18n'; interface Props { handleChangeValue: (event: SelectChangeEvent) => void; @@ -20,9 +19,6 @@ const ClickableChipPopover: FunctionComponent = ({ element, }) => { // Standard hooks - const { t } = useFormatter(); - - console.log(availableValues); const displayOperatorAndFilter = () => { // Specific field From e5125483a57459af0ac41b671907bb15144cc565 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 2 Oct 2024 17:02:53 +0200 Subject: [PATCH 06/34] [frontend/backend] Chaining injects conditionally --- openbas-front/src/utils/Localization.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openbas-front/src/utils/Localization.js b/openbas-front/src/utils/Localization.js index fe6f62b789..b055683651 100644 --- a/openbas-front/src/utils/Localization.js +++ b/openbas-front/src/utils/Localization.js @@ -1394,6 +1394,9 @@ const i18n = { 'The element has been successfully updated': 'L\'élément a été mis à jour avec succès', 'The element has been successfully deleted': 'L\'élément a été supprimé avec succès', 'No data to display': 'Aucune donnée à afficher', + 'Add condition': 'Ajouter une condition', + is: 'est', + undefined: 'non défini', }, zh: { 'Email address': 'email地址', @@ -2734,6 +2737,9 @@ const i18n = { 'The element has been successfully deleted': '元素已成功删除', 'Internal error': '内部错误 ', 'No data to display': '没有可显示的数据', + 'Add condition': '添加条件', + is: '是', + undefined: '未定义', }, en: { openbas_email: 'Email', From 1c2f357a64b9ebc35c94eed12c3f63a9940801a8 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 2 Oct 2024 17:50:25 +0200 Subject: [PATCH 07/34] [frontend/backend] Chaining injects conditionally --- .../src/main/java/io/openbas/service/AtomicTestingService.java | 2 +- .../src/main/java/io/openbas/database/model/Inject.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java index f947391e3c..4356a8ab65 100644 --- a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java +++ b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java @@ -228,7 +228,7 @@ public Inject copyInject(@NotNull Inject injectOrigin, boolean isAtomic) { injectDuplicate.setTeams(injectOrigin.getTeams().stream().toList()); injectDuplicate.setEnabled(injectOrigin.isEnabled()); injectDuplicate.setDependsDuration(injectOrigin.getDependsDuration()); - injectDuplicate.setDependsOn(injectOrigin.getDependsOn()); + injectDuplicate.setDependsOn(injectOrigin.getDependsOn().stream().toList()); injectDuplicate.setCountry(injectOrigin.getCountry()); injectDuplicate.setCity(injectOrigin.getCity()); injectDuplicate.setInjectorContract(injectOrigin.getInjectorContract().orElse(null)); diff --git a/openbas-model/src/main/java/io/openbas/database/model/Inject.java b/openbas-model/src/main/java/io/openbas/database/model/Inject.java index bc3838f6d7..4e21c6aeb0 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Inject.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Inject.java @@ -123,7 +123,7 @@ public class Inject implements Base, Injection { private Scenario scenario; @Getter - @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JsonProperty("inject_depends_on") private List dependsOn; From 3ce3eb56b4e42325e4a165b344290566abc3cc81 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Thu, 3 Oct 2024 09:48:37 +0200 Subject: [PATCH 08/34] [frontend/backend] Chaining injects conditionally --- .../main/java/io/openbas/service/AtomicTestingService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java index 4356a8ab65..8aca82e63e 100644 --- a/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java +++ b/openbas-api/src/main/java/io/openbas/service/AtomicTestingService.java @@ -228,7 +228,9 @@ public Inject copyInject(@NotNull Inject injectOrigin, boolean isAtomic) { injectDuplicate.setTeams(injectOrigin.getTeams().stream().toList()); injectDuplicate.setEnabled(injectOrigin.isEnabled()); injectDuplicate.setDependsDuration(injectOrigin.getDependsDuration()); - injectDuplicate.setDependsOn(injectOrigin.getDependsOn().stream().toList()); + if(injectOrigin.getDependsOn() != null) { + injectDuplicate.setDependsOn(injectOrigin.getDependsOn().stream().toList()); + } injectDuplicate.setCountry(injectOrigin.getCountry()); injectDuplicate.setCity(injectOrigin.getCity()); injectDuplicate.setInjectorContract(injectOrigin.getInjectorContract().orElse(null)); From 72aa26020c6c81ab9ca3f9f84040e4f75036510a Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Thu, 3 Oct 2024 18:30:49 +0200 Subject: [PATCH 09/34] [frontend/backend] Chaining injects conditionally --- .../io/openbas/rest/inject/InjectApi.java | 2 +- .../scheduler/jobs/InjectsExecutionJob.java | 26 ++++++++++++------- .../common/injects/InjectChainsForm.js | 2 +- .../components/common/injects/Injects.tsx | 5 +++- .../injects/UpdateInjectLogicalChains.js | 1 - .../io/openbas/database/model/Inject.java | 2 +- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java index 08f5437b53..f369968d88 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java @@ -611,7 +611,7 @@ private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInpu } List injectDepencyToRemove = new ArrayList<>(); - if(input.getDependsOn() != null) { + if(input.getDependsOn() != null && !input.getDependsOn().isEmpty()) { inject.getDependsOn().forEach( injectDependency -> { if (!input.getDependsOn().keySet().contains(injectDependency.getCompositeId().getInjectParentId().getId())) { diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index b7378b61c4..8114551968 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -66,6 +66,7 @@ public class InjectsExecutionJob implements Job { private final AtomicTestingService atomicTestingService; private final InjectDependenciesRepository injectDependenciesRepository; private final ExerciseExpectationService exerciseExpectationService; + private final InjectExpectationRepository injectExpectationRepository; private static final String EXPECTATIONS = "expectations"; @@ -259,13 +260,15 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I List results = null; for (InjectDependency injectDependency : injectDependencies) { - String expressionToEvaluate = injectDependency.getCondition() - .replaceAll("(.*-Execution-Success)", "#this['$1']"); + String expressionToEvaluate = injectDependency.getCondition(); + List conditions = Arrays.stream(injectDependency.getCondition() + .split("(&&|\\|\\|)")).toList(); + for(String condition : conditions) { + expressionToEvaluate = expressionToEvaluate.replaceAll(condition.trim(), condition.trim().replaceAll("(.*-Success)", "#this['$1']")); + } ExpressionParser parser = new SpelExpressionParser(); - EvaluationContext context = new StandardEvaluationContext(mapCondition); - Expression exp = parser.parseExpression(String.format(expressionToEvaluate, - injectDependency.getCompositeId().getInjectParentId() + "-Execution-Success")); + Expression exp = parser.parseExpression(expressionToEvaluate); boolean canBeExecuted = Boolean.TRUE.equals(exp.getValue(mapCondition, Boolean.class)); if (!canBeExecuted) { if (results == null) { @@ -282,8 +285,6 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I private @NotNull Map getStringBooleanMap(List parents, String exerciseId, List injectDependencies) { Map mapCondition = new HashMap<>(); - List expectations = this.exerciseExpectationService.injectExpectations(exerciseId); - injectDependencies.forEach(injectDependency -> { List splittedConditions = List.of(injectDependency.getCondition().split("&&|\\|\\|")); splittedConditions.forEach(condition -> { @@ -297,12 +298,19 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I && !parent.getStatus().get().getName().equals(ExecutionStatus.ERROR) && !executionStatusesNotReady.contains(parent.getStatus().get().getName())); - parent.getExpectations().forEach(injectExpectation -> { + List expectations = injectExpectationRepository.findAllForExerciseAndInject(exerciseId, parent.getId()); + expectations.forEach(injectExpectation -> { String name = String.format("%s-%s-Success", parent.getId(), StringUtils.capitalize(injectExpectation.getType().toString().toLowerCase())); if(injectExpectation.getType().equals(InjectExpectation.EXPECTATION_TYPE.MANUAL)) { name = String.format("%s-%s-Success", parent.getId(), injectExpectation.getName()); } - mapCondition.put(name, expectationStatusesSuccess.contains(injectExpectation.getResponse())); + if(InjectExpectation.EXPECTATION_TYPE.CHALLENGE.equals(injectExpectation.getType())) { + if(injectExpectation.getUser() == null) { + mapCondition.put(name, injectExpectation.getScore() >= injectExpectation.getExpectedScore()); + } + } else { + mapCondition.put(name, expectationStatusesSuccess.contains(injectExpectation.getResponse())); + } }); }); return mapCondition; diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js index 85168c1c8b..8d5ddf572e 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js @@ -247,7 +247,7 @@ const InjectForm = ({ let writtenCondition = ''; for (const conditionElementIndex in conditions[dependency].conditionElement) { if (Object.hasOwn(conditions[dependency].conditionElement, conditionElementIndex)) { - writtenCondition += `${conditionElementIndex > 0 ? '&& ' : ''}${conditions[dependency].conditionElement[conditionElementIndex].name} == ${conditions[dependency].conditionElement[conditionElementIndex].value}`; + writtenCondition += `${conditionElementIndex > 0 ? ' && ' : ''}${conditions[dependency].conditionElement[conditionElementIndex].name} == ${conditions[dependency].conditionElement[conditionElementIndex].value}`; } } newConditions[conditions[dependency].parentId] = writtenCondition; diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index 42b59945d9..a4e0f35f71 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -219,7 +219,10 @@ const Injects: FunctionComponent = ({ const onUpdate = (result: { result: string, entities: { injects: Record } }) => { if (result.entities) { const updated = result.entities.injects[result.result]; - setInjects(injects.map((i) => (i.inject_id !== updated.inject_id ? i as InjectOutputType : (updated as InjectOutputType)))); + setInjects(injects.map((i) => { + console.log((i.inject_id !== updated.inject_id ? i as InjectOutputType : (updated as InjectOutputType))); + return (i.inject_id !== updated.inject_id ? i as InjectOutputType : (updated as InjectOutputType)); + })); } }; diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js index 576847181f..ad21544ba8 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.js @@ -97,7 +97,6 @@ const UpdateInjectLogicalChains = ({ injectsToUpdate.push(injectChildrenUpdate); } }); - await onUpdateInject([injectUpdate, ...injectsToUpdate]); handleClose(); diff --git a/openbas-model/src/main/java/io/openbas/database/model/Inject.java b/openbas-model/src/main/java/io/openbas/database/model/Inject.java index 4e21c6aeb0..569649cd54 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Inject.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Inject.java @@ -123,7 +123,7 @@ public class Inject implements Base, Injection { private Scenario scenario; @Getter - @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.ALL) @JsonProperty("inject_depends_on") private List dependsOn; From 03ca9a18eabed89ad2a9d05bd4832ccdb68c98a3 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Thu, 3 Oct 2024 20:55:37 +0200 Subject: [PATCH 10/34] [frontend/backend] Chaining injects conditionally --- .../java/io/openbas/scheduler/jobs/InjectsExecutionJob.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index 8114551968..c33de79784 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -288,7 +288,7 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I injectDependencies.forEach(injectDependency -> { List splittedConditions = List.of(injectDependency.getCondition().split("&&|\\|\\|")); splittedConditions.forEach(condition -> { - mapCondition.put(injectDependency.getCondition().split("==")[0].trim(), false); + mapCondition.put(condition.split("==")[0].trim(), false); }); }); @@ -304,7 +304,8 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I if(injectExpectation.getType().equals(InjectExpectation.EXPECTATION_TYPE.MANUAL)) { name = String.format("%s-%s-Success", parent.getId(), injectExpectation.getName()); } - if(InjectExpectation.EXPECTATION_TYPE.CHALLENGE.equals(injectExpectation.getType())) { + if(InjectExpectation.EXPECTATION_TYPE.CHALLENGE.equals(injectExpectation.getType()) + || InjectExpectation.EXPECTATION_TYPE.ARTICLE.equals(injectExpectation.getType())) { if(injectExpectation.getUser() == null) { mapCondition.put(name, injectExpectation.getScore() >= injectExpectation.getExpectedScore()); } From b3c7f636ced2a157b13c635ecd392b3e2ff38dc1 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Fri, 4 Oct 2024 16:11:04 +0200 Subject: [PATCH 11/34] [frontend/backend] Chaining injects conditionally --- .../java/io/openbas/scheduler/jobs/InjectsExecutionJob.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index c33de79784..4c690316b4 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -306,7 +306,7 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I } if(InjectExpectation.EXPECTATION_TYPE.CHALLENGE.equals(injectExpectation.getType()) || InjectExpectation.EXPECTATION_TYPE.ARTICLE.equals(injectExpectation.getType())) { - if(injectExpectation.getUser() == null) { + if(injectExpectation.getUser() == null && injectExpectation.getScore() != null) { mapCondition.put(name, injectExpectation.getScore() >= injectExpectation.getExpectedScore()); } } else { From 19ec3782d4bc6045a2d2bb3383cc3ca16ab5368b Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Fri, 11 Oct 2024 14:58:25 +0200 Subject: [PATCH 12/34] [frontend/backend] Chaining injects conditionally --- openbas-front/src/admin/components/common/injects/Injects.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index a4e0f35f71..795c375846 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -220,7 +220,6 @@ const Injects: FunctionComponent = ({ if (result.entities) { const updated = result.entities.injects[result.result]; setInjects(injects.map((i) => { - console.log((i.inject_id !== updated.inject_id ? i as InjectOutputType : (updated as InjectOutputType))); return (i.inject_id !== updated.inject_id ? i as InjectOutputType : (updated as InjectOutputType)); })); } From 52bb8a0ca61a6c1fe26738268248d64ea2f65d80 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Fri, 11 Oct 2024 16:36:18 +0200 Subject: [PATCH 13/34] [frontend/backend] Chaining injects conditionally --- .../src/main/java/io/openbas/database/model/Inject.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbas-model/src/main/java/io/openbas/database/model/Inject.java b/openbas-model/src/main/java/io/openbas/database/model/Inject.java index 569649cd54..4e21c6aeb0 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Inject.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Inject.java @@ -123,7 +123,7 @@ public class Inject implements Base, Injection { private Scenario scenario; @Getter - @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.ALL) + @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JsonProperty("inject_depends_on") private List dependsOn; From facf4dc464d90caa34fb880622d8a436ea73b4be Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Fri, 11 Oct 2024 17:21:40 +0200 Subject: [PATCH 14/34] [frontend/backend] Chaining injects conditionally --- .../src/main/java/io/openbas/database/model/Inject.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openbas-model/src/main/java/io/openbas/database/model/Inject.java b/openbas-model/src/main/java/io/openbas/database/model/Inject.java index 4e21c6aeb0..6f384417e5 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Inject.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Inject.java @@ -123,9 +123,9 @@ public class Inject implements Base, Injection { private Scenario scenario; @Getter - @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.ALL) @JsonProperty("inject_depends_on") - private List dependsOn; + private List dependsOn = new ArrayList<>(); @Getter @Column(name = "inject_depends_duration") From 9148d261572e60e9121fa8d9575fa401c820969d Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Fri, 11 Oct 2024 17:30:12 +0200 Subject: [PATCH 15/34] [frontend/backend] Chaining injects conditionally --- .../java/io/openbas/service/ScenarioToExerciseService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java b/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java index 14daf17b13..91165b891e 100644 --- a/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java +++ b/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java @@ -247,7 +247,8 @@ public Exercise toExercise( scenarioInjects.forEach(scenarioInject -> { if(scenarioInject.getDependsOn() != null) { Inject injectToUpdate = mapExerciseInjectsByScenarioInject.get(scenarioInject.getId()); - injectToUpdate.setDependsOn(scenarioInject.getDependsOn()); + injectToUpdate.getDependsOn().clear(); + injectToUpdate.getDependsOn().addAll(scenarioInject.getDependsOn()); this.injectRepository.save(injectToUpdate); } }); From 9863aa7c5bc14dae881c0a3f236f9c6d8ded3825 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Fri, 11 Oct 2024 22:27:29 +0200 Subject: [PATCH 16/34] [frontend/backend] Chaining injects conditionally --- .../src/admin/components/common/injects/Injects.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index 795c375846..2553439bfd 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -256,7 +256,17 @@ const Injects: FunctionComponent = ({ promises.push(injectContext.onUpdateInject(inject.inject_id, inject).then((result: { result: string, entities: { injects: Record } }) => { if (result.entities) { onUpdate(result); - return result.entities.injects[result.result]; + const dependingOn : Record = {}; + result.entities.injects[result.result].inject_depends_on?.forEach((value) => { + if (value.dependency_condition != null && value.dependency_relationship?.inject_parent_id !== undefined) { + dependingOn[value.dependency_relationship?.inject_parent_id as unknown as string] = value.dependency_condition; + } + }); + const newResult = { + ...result.entities.injects[result.result], + inject_depends_on: dependingOn, + }; + return newResult as never; } return undefined; })); From 78675bedc21d75825f9888360ea0ccfd2e68e206 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Fri, 11 Oct 2024 22:36:40 +0200 Subject: [PATCH 17/34] [frontend/backend] Chaining injects conditionally --- openbas-front/src/admin/components/common/injects/Injects.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index 2553439bfd..e3bf52c562 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -49,7 +49,6 @@ const useStyles = makeStyles(() => ({ color: '#00b1ff', border: '1px solid #00b1ff', }, - itemHead: { textTransform: 'uppercase', }, From 8ac59caeeef38e52f95a70ea7fb4a6d6d3c44bd7 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Sun, 13 Oct 2024 00:21:51 +0200 Subject: [PATCH 18/34] [frontend/backend] Chaining injects conditionally --- openbas-front/src/admin/components/common/injects/Injects.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index e3bf52c562..e9834616ee 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -215,6 +215,7 @@ const Injects: FunctionComponent = ({ setInjects([created as InjectOutputType, ...injects]); } }; + const onUpdate = (result: { result: string, entities: { injects: Record } }) => { if (result.entities) { const updated = result.entities.injects[result.result]; From 149de854b68fc694d1ccd04a31d9359ff8fb493f Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Sun, 13 Oct 2024 00:33:19 +0200 Subject: [PATCH 19/34] [frontend/backend] Chaining injects conditionally --- .../io/openbas/scheduler/jobs/InjectsExecutionJob.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index 4c690316b4..c11321a2d3 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -1,19 +1,12 @@ package io.openbas.scheduler.jobs; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.openbas.asset.QueueService; import io.openbas.database.model.*; import io.openbas.database.repository.*; import io.openbas.execution.ExecutableInject; import io.openbas.execution.ExecutionExecutorService; import io.openbas.helper.InjectHelper; -import io.openbas.inject_expectation.InjectExpectationService; -import io.openbas.injector_contract.ContractType; -import io.openbas.model.Expectation; import io.openbas.service.AtomicTestingService; import io.openbas.service.ExerciseExpectationService; import jakarta.annotation.Resource; @@ -28,16 +21,13 @@ import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; -import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import java.time.Instant; import java.util.*; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; From 30505fc248ea5afbd581ab192cca9c729c13b0f5 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Sun, 13 Oct 2024 00:39:15 +0200 Subject: [PATCH 20/34] [frontend/backend] Chaining injects conditionally --- .../scheduler/jobs/InjectsExecutionJob.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index c11321a2d3..4a54e4b578 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -55,11 +55,8 @@ public class InjectsExecutionJob implements Job { private final ExecutionExecutorService executionExecutorService; private final AtomicTestingService atomicTestingService; private final InjectDependenciesRepository injectDependenciesRepository; - private final ExerciseExpectationService exerciseExpectationService; private final InjectExpectationRepository injectExpectationRepository; - private static final String EXPECTATIONS = "expectations"; - private final List executionStatusesNotReady = List.of(ExecutionStatus.QUEUING, ExecutionStatus.DRAFT, ExecutionStatus.EXECUTING, ExecutionStatus.PENDING); @@ -238,6 +235,12 @@ private void executeInject(ExecutableInject executableInject) { } } + /** + * Get error messages if pre execution conditions are not met + * @param exerciseId the id of the exercise + * @param inject the inject to check + * @return an optional of list of error message + */ private Optional> getErrorMessagesPreExecution(String exerciseId, Inject inject) { List injectDependencies = injectDependenciesRepository.findParents(List.of(inject.getId())); if (!injectDependencies.isEmpty()) { @@ -272,6 +275,13 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I return Optional.empty(); } + /** + * Get a map containing the expectations and if they are met or not + * @param parents the parents injects + * @param exerciseId the id of the exercise + * @param injectDependencies the list of dependencies + * @return a map of expectations and their value + */ private @NotNull Map getStringBooleanMap(List parents, String exerciseId, List injectDependencies) { Map mapCondition = new HashMap<>(); From d413b5797cdb9721b0678e29645355668c114c1d Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Sun, 13 Oct 2024 00:46:10 +0200 Subject: [PATCH 21/34] [frontend/backend] Chaining injects conditionally --- .../common/injects/InjectChainsForm.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js index 8d5ddf572e..d7c411a4bf 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js @@ -28,6 +28,15 @@ const InjectForm = ({ }) => { const classes = useStyles(); const { t } = useFormatter(); + + const breakpointAndOr = /&&|\|\|/gm; + const breakpointValue = /==/gm; + const typeFromName = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-(.*)-Success/mg; + + const capitalize = (text) => { + return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); + }; + const [parents, setParents] = useState( injects.filter((currentInject) => values.inject_depends_on !== null && values.inject_depends_on[currentInject.inject_id] !== undefined) @@ -44,9 +53,6 @@ const InjectForm = ({ ); const getConditionContentParent = (injectDependsOn) => { - const breakpointAndOr = /&&|\|\|/gm; - const breakpointValue = /==/gm; - const typeFromName = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-(.*)-Success/mg; const conditions = []; for (const dependency in injectDependsOn) { if (Object.hasOwn(injectDependsOn, dependency)) { @@ -71,9 +77,6 @@ const InjectForm = ({ }; const getConditionContentChildren = (injectDependsTo) => { - const breakpointAndOr = /&&|\|\|/gm; - const breakpointValue = /==/gm; - const typeFromName = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-(.*)-Success/mg; const conditions = []; for (const children in injectDependsTo) { if (Object.hasOwn(injectDependsTo, children)) { @@ -392,10 +395,6 @@ const InjectForm = ({ ); }; - const capitalize = (text) => { - return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); - }; - const getAvailableExpectations = (inject) => { if (inject.inject_content !== null && inject.inject_content !== undefined) { const expectations = inject.inject_content.expectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); From 33ba7bf302e68786546e71d3c69d9df81b38e689 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 16 Oct 2024 13:56:09 +0200 Subject: [PATCH 22/34] [frontend/backend] Fixes following PR comments --- .../io/openbas/rest/inject/InjectApi.java | 1 - .../common/injects/InjectChainsForm.js | 65 ++++++++++--------- .../components/common/injects/Injects.tsx | 2 +- openbas-front/src/utils/String.js | 4 ++ 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java index f369968d88..7cfa4c8826 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java @@ -88,7 +88,6 @@ public class InjectApi extends RestBehavior { private final InjectService injectService; private final AtomicTestingService atomicTestingService; private final InjectDuplicateService injectDuplicateService; - private final InjectDependenciesRepository injectDependenciesRepository; // -- INJECTS -- diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js index d7c411a4bf..04b2760a57 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js @@ -5,6 +5,7 @@ import { Add, DeleteOutlined, ExpandMore } from '@mui/icons-material'; import { useFormatter } from '../../../../components/i18n'; import ClickableModeChip from '../../../../components/common/chips/ClickableModeChip'; import ClickableChip from '../../../../components/common/chips/ClickableChip'; +import { capitalize } from '../../../../utils/String'; const useStyles = makeStyles(() => ({ container: { @@ -33,10 +34,6 @@ const InjectForm = ({ const breakpointValue = /==/gm; const typeFromName = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-(.*)-Success/mg; - const capitalize = (text) => { - return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); - }; - const [parents, setParents] = useState( injects.filter((currentInject) => values.inject_depends_on !== null && values.inject_depends_on[currentInject.inject_id] !== undefined) @@ -570,20 +567,22 @@ const InjectForm = ({ > {getClickableParentChip(parent)} - +
+ +
@@ -661,20 +660,22 @@ const InjectForm = ({ > {getClickableChildrenChip(children)} - +
+ +
diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index e9834616ee..490d51be8c 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -392,7 +392,7 @@ const Injects: FunctionComponent = ({ 'inject_description', 'inject_injector_contract', 'inject_content', - 'inject_depends_from_another', + 'inject_depends_on', 'inject_depends_duration', 'inject_teams', 'inject_assets', diff --git a/openbas-front/src/utils/String.js b/openbas-front/src/utils/String.js index f1a589b5c5..a9ad73a527 100644 --- a/openbas-front/src/utils/String.js +++ b/openbas-front/src/utils/String.js @@ -68,6 +68,10 @@ export const computeLabel = (status) => { return 'Failed'; }; +export const capitalize = (text) => { + return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); +}; + // compute color for status export const computeColorStyle = (status) => { if (status === 'PENDING') { From cc9884801df9db826bd4fc6b496b167d9957e8f2 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 16 Oct 2024 16:20:45 +0200 Subject: [PATCH 23/34] [frontend/backend] Fixes following PR comments --- .../io/openbas/rest/inject/InjectApi.java | 16 +++---- .../rest/inject/output/InjectOutput.java | 2 +- .../scheduler/jobs/InjectsExecutionJob.java | 15 ++++++- .../common/injects/InjectChainsForm.js | 42 ++++++++++++------- .../components/common/injects/Injects.tsx | 15 ++----- .../common/injects/UpdateInjectDetails.js | 4 +- .../common/injects/chaining/ChainingUtils.tsx | 18 +++++++- .../src/components/ChainedTimeline.tsx | 3 ++ .../io/openbas/database/model/Inject.java | 2 +- .../database/model/InjectDependencyId.java | 8 ++-- 10 files changed, 78 insertions(+), 47 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java index 7cfa4c8826..7b447a88da 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java @@ -301,8 +301,8 @@ public Inject createInjectForExercise(@PathVariable String exerciseId, @Valid @R if(input.getDependsOn() != null) { inject.setDependsOn(input.getDependsOn().entrySet().stream().map(entry -> { InjectDependency injectDependency = new InjectDependency(); - injectDependency.getCompositeId().setInjectChildrenId(injectRepository.findById(entry.getKey()).orElse(null)); - injectDependency.getCompositeId().setInjectParentId(inject); + injectDependency.getCompositeId().setInjectChildren(injectRepository.findById(entry.getKey()).orElse(null)); + injectDependency.getCompositeId().setInjectParent(inject); injectDependency.setCondition(entry.getValue()); return injectDependency; }).toList()); @@ -479,8 +479,8 @@ public Inject createInjectForScenario( if(input.getDependsOn() != null) { inject.setDependsOn(input.getDependsOn().entrySet().stream().map(entry -> { InjectDependency injectDependency = new InjectDependency(); - injectDependency.getCompositeId().setInjectChildrenId(injectRepository.findById(entry.getKey()).orElse(null)); - injectDependency.getCompositeId().setInjectParentId(inject); + injectDependency.getCompositeId().setInjectChildren(injectRepository.findById(entry.getKey()).orElse(null)); + injectDependency.getCompositeId().setInjectParent(inject); injectDependency.setCondition(entry.getValue()); return injectDependency; }).toList()); @@ -595,14 +595,14 @@ private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInpu if(input.getDependsOn() != null) { input.getDependsOn().entrySet().forEach(entry -> { Optional existingDependency = inject.getDependsOn().stream() - .filter(injectDependency -> injectDependency.getCompositeId().getInjectParentId().getId().equals(entry.getKey())) + .filter(injectDependency -> injectDependency.getCompositeId().getInjectParent().getId().equals(entry.getKey())) .findFirst(); if(existingDependency.isPresent()) { existingDependency.get().setCondition(entry.getValue()); } else { InjectDependency injectDependency = new InjectDependency(); - injectDependency.getCompositeId().setInjectChildrenId(inject); - injectDependency.getCompositeId().setInjectParentId(injectRepository.findById(entry.getKey()).orElse(null)); + injectDependency.getCompositeId().setInjectChildren(inject); + injectDependency.getCompositeId().setInjectParent(injectRepository.findById(entry.getKey()).orElse(null)); injectDependency.setCondition(entry.getValue()); inject.getDependsOn().add(injectDependency); } @@ -613,7 +613,7 @@ private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInpu if(input.getDependsOn() != null && !input.getDependsOn().isEmpty()) { inject.getDependsOn().forEach( injectDependency -> { - if (!input.getDependsOn().keySet().contains(injectDependency.getCompositeId().getInjectParentId().getId())) { + if (!input.getDependsOn().keySet().contains(injectDependency.getCompositeId().getInjectParent().getId())) { injectDepencyToRemove.add(injectDependency); } } diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java index a27343b312..573decf044 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java @@ -116,7 +116,7 @@ public InjectOutput( if (injectDependency != null) { this.dependsOn = Stream.of(injectDependency) - .collect(Collectors.toMap(injectDep -> injectDep.getCompositeId().getInjectParentId().getId(), InjectDependency::getCondition)); + .collect(Collectors.toMap(injectDep -> injectDep.getCompositeId().getInjectParent().getId(), InjectDependency::getCondition)); } } diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index 4a54e4b578..ed7b6bf5f4 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -245,7 +245,7 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I List injectDependencies = injectDependenciesRepository.findParents(List.of(inject.getId())); if (!injectDependencies.isEmpty()) { List parents = StreamSupport.stream(injectRepository.findAllById(injectDependencies.stream() - .map(injectDependency -> injectDependency.getCompositeId().getInjectParentId().getId()).toList()).spliterator(), false) + .map(injectDependency -> injectDependency.getCompositeId().getInjectParent().getId()).toList()).spliterator(), false) .collect(Collectors.toList()); Map mapCondition = getStringBooleanMap(parents, exerciseId, injectDependencies); @@ -266,8 +266,9 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I if (!canBeExecuted) { if (results == null) { results = new ArrayList<>(); + results.add("This inject depends on other injects expectations that are not met. The following conditions were not as expected : "); } - results.add("The conditions to launch this inject are not met"); + results.addAll(labelFromCondition(injectDependency.getCompositeId().getInjectParent(), injectDependency.getCondition())); } } return results == null ? Optional.empty() : Optional.of(results); @@ -317,6 +318,16 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I return mapCondition; } + private List labelFromCondition(Inject injectParent, String condition) { + List result = new ArrayList<>(); + List conditionElements = List.of(condition.split("(&&|\\|\\|)")); + for (String conditionElement : conditionElements) { + String type = conditionElement.split("==")[0].trim().replaceAll(injectParent.getId() + "-", "").replaceAll("-Success", ""); + result.add(String.format("Inject '%s' - %s is %s", injectParent.getTitle(), type, conditionElement.endsWith("true") ? "true" : "false")); + } + return result; + } + public void updateExercise(String exerciseId) { Exercise exercise = exerciseRepository.findById(exerciseId).orElseThrow(); exercise.setUpdatedAt(now()); diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js index 04b2760a57..2207ac0eec 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { makeStyles } from '@mui/styles'; import { Accordion, AccordionDetails, AccordionSummary, Box, Button, FormControl, IconButton, InputLabel, MenuItem, Select, Tooltip, Typography } from '@mui/material'; import { Add, DeleteOutlined, ExpandMore } from '@mui/icons-material'; @@ -49,6 +49,12 @@ const InjectForm = ({ }), ); + const [addChildrenButtonDisabled, setAddChildrenButtonDisabled] = useState(false); + useEffect(() => { + const availableChildrensNumber = injects.filter((currentInject) => currentInject.inject_depends_duration > values.inject_depends_duration).length; + setAddChildrenButtonDisabled(childrens.length >= availableChildrensNumber); + }, [childrens]); + const getConditionContentParent = (injectDependsOn) => { const conditions = []; for (const dependency in injectDependsOn) { @@ -80,20 +86,22 @@ const InjectForm = ({ for (const dependency in injectDependsTo[children]) { if (Object.hasOwn(injectDependsTo[children], dependency)) { const condition = injectDependsTo[children][dependency][values.inject_id]; - const splittedConditions = condition.split(breakpointAndOr); - conditions.push({ - childrenId: dependency, - mode: condition.includes('||') ? 'or' : 'and', - conditionElement: splittedConditions.map((splitedCondition, index) => { - const key = Array.from(splitedCondition.split(breakpointValue)[0].trim().matchAll(typeFromName), (m) => m[1]); - return { - name: splitedCondition.split(breakpointValue)[0].trim(), - value: splitedCondition.split(breakpointValue)[1].trim(), - key: key[0], - index, - }; - }), - }); + if (condition !== undefined) { + const splittedConditions = condition.split(breakpointAndOr); + conditions.push({ + childrenId: dependency, + mode: condition.includes('||') ? 'or' : 'and', + conditionElement: splittedConditions.map((splitedCondition, index) => { + const key = Array.from(splitedCondition.split(breakpointValue)[0].trim().matchAll(typeFromName), (m) => m[1]); + return { + name: splitedCondition.split(breakpointValue)[0].trim(), + value: splitedCondition.split(breakpointValue)[1].trim(), + key: key[0], + index, + }; + }), + }); + } } } } @@ -505,7 +513,8 @@ const InjectForm = ({ color="secondary" aria-label="Add" size="large" - disabled={parents.length > 0} + disabled={parents.length > 0 + || injects.filter((currentInject) => currentInject.inject_depends_duration < values.inject_depends_duration).length === 0} onClick={addParent} > @@ -597,6 +606,7 @@ const InjectForm = ({ color="secondary" aria-label="Add" size="large" + disabled={addChildrenButtonDisabled} onClick={addChildren} > diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index 490d51be8c..cbcece3427 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -31,6 +31,7 @@ import { useQueryableWithLocalStorage } from '../../../../components/common/quer import ToolBar from '../ToolBar'; import { MESSAGING$ } from '../../../../utils/Environment'; import useEntityToggle from '../../../../utils/hooks/useEntityToggle'; +import chainingUtils from './chaining/ChainingUtils'; const useStyles = makeStyles(() => ({ disabled: { @@ -245,7 +246,7 @@ const Injects: FunctionComponent = ({ const onUpdateInject = async (data: Inject) => { if (selectedInjectId) { await injectContext.onUpdateInject(selectedInjectId, data).then((result: { result: string, entities: { injects: Record } }) => { - onUpdate(result); + onUpdate(chainingUtils.convertInjectStore(result.entities.injects[result.result]) as never); }); } }; @@ -256,17 +257,7 @@ const Injects: FunctionComponent = ({ promises.push(injectContext.onUpdateInject(inject.inject_id, inject).then((result: { result: string, entities: { injects: Record } }) => { if (result.entities) { onUpdate(result); - const dependingOn : Record = {}; - result.entities.injects[result.result].inject_depends_on?.forEach((value) => { - if (value.dependency_condition != null && value.dependency_relationship?.inject_parent_id !== undefined) { - dependingOn[value.dependency_relationship?.inject_parent_id as unknown as string] = value.dependency_condition; - } - }); - const newResult = { - ...result.entities.injects[result.result], - inject_depends_on: dependingOn, - }; - return newResult as never; + return chainingUtils.convertInjectStore(result.entities.injects[result.result]) as never; } return undefined; })); diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js index 2c18d11d0b..077b7b8aa3 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js @@ -18,7 +18,7 @@ import { isEmptyField } from '../../../../utils/utils'; import { tagOptions } from '../../../../utils/Option'; import { splitDuration } from '../../../../utils/Time'; import PlatformIcon from '../../../../components/PlatformIcon'; -import fromInjectDependencyToInputDependency from './chaining/ChainingUtils'; +import chainingUtils from './chaining/ChainingUtils'; const useStyles = makeStyles((theme) => ({ details: { @@ -210,7 +210,7 @@ const UpdateInjectDetails = ({ inject_asset_groups: assetGroupIds, inject_documents: documents, inject_depends_duration, - inject_depends_on: fromInjectDependencyToInputDependency(data.inject_depends_on), + inject_depends_on: chainingUtils.fromInjectDependencyToInputDependency(data.inject_depends_on), }; await onUpdateInject(values); } diff --git a/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx b/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx index c51683d916..197f679c75 100644 --- a/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx +++ b/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx @@ -1,4 +1,5 @@ import type { InjectDependency } from '../../../../../utils/api-types'; +import type { InjectStore } from '../../../../../actions/injects/Inject'; const fromInjectDependencyToInputDependency = (dependencies: InjectDependency[]) => { const result : Record = {}; @@ -14,4 +15,19 @@ const fromInjectDependencyToInputDependency = (dependencies: InjectDependency[]) return dependencies.length > 0 ? result : null; }; -export default fromInjectDependencyToInputDependency; +const convertInjectStore = (injectStore: InjectStore) => { + const dependingOn : Record = {}; + injectStore.inject_depends_on?.forEach((value) => { + if (value.dependency_condition != null && value.dependency_relationship?.inject_parent_id !== undefined) { + dependingOn[value.dependency_relationship?.inject_parent_id as unknown as string] = value.dependency_condition; + } + }); + const newResult = { + ...injectStore, + inject_depends_on: Object.keys(dependingOn).length === 0 ? null : dependingOn, + }; + + return newResult; +}; + +export default { fromInjectDependencyToInputDependency, convertInjectStore }; diff --git a/openbas-front/src/components/ChainedTimeline.tsx b/openbas-front/src/components/ChainedTimeline.tsx index 160bc62e83..5a7d2b726a 100644 --- a/openbas-front/src/components/ChainedTimeline.tsx +++ b/openbas-front/src/components/ChainedTimeline.tsx @@ -37,6 +37,7 @@ import { useFormatter } from './i18n'; import type { AssetGroupsHelper } from '../actions/asset_groups/assetgroup-helper'; import type { EndpointHelper } from '../actions/assets/asset-helper'; import type { Inject } from '../utils/api-types'; +import chainingUtils from '../admin/components/common/injects/chaining/ChainingUtils'; const useStyles = makeStyles(() => ({ container: { @@ -286,6 +287,8 @@ const ChainedTimelineFlow: FunctionComponent = ({ inject_injector_contract: injectFromMap.inject_injector_contract.injector_contract_id, inject_id: node.id, inject_depends_duration: convertCoordinatesToTime(node.position), + inject_depends_on: injectFromMap.inject_depends_on !== null + ? chainingUtils.fromInjectDependencyToInputDependency(injectFromMap.inject_depends_on) : null, }; onUpdateInject([inject]); setCurrentUpdatedNode(node); diff --git a/openbas-model/src/main/java/io/openbas/database/model/Inject.java b/openbas-model/src/main/java/io/openbas/database/model/Inject.java index 6f384417e5..e3a93e53a6 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/Inject.java +++ b/openbas-model/src/main/java/io/openbas/database/model/Inject.java @@ -123,7 +123,7 @@ public class Inject implements Base, Injection { private Scenario scenario; @Getter - @OneToMany(mappedBy = "compositeId.injectChildrenId", fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.ALL) + @OneToMany(mappedBy = "compositeId.injectChildren", fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.ALL) @JsonProperty("inject_depends_on") private List dependsOn = new ArrayList<>(); diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java index 907b9513d7..f6806f2463 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java @@ -27,24 +27,24 @@ public class InjectDependencyId implements Serializable { @JsonProperty("inject_parent_id") @JoinColumn(referencedColumnName="inject_id", name="inject_parent_id") @JsonSerialize(using = MonoIdDeserializer.class) - private Inject injectParentId; + private Inject injectParent; @ManyToOne @JsonProperty("inject_children_id") @JoinColumn(referencedColumnName="inject_id", name="inject_children_id") @JsonSerialize(using = MonoIdDeserializer.class) - private Inject injectChildrenId; + private Inject injectChildren; @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; InjectDependencyId that = (InjectDependencyId) o; - return injectParentId.equals(that.injectParentId) && injectChildrenId.equals(that.injectChildrenId); + return injectParent.equals(that.injectParent) && injectChildren.equals(that.injectChildren); } @Override public int hashCode() { - return Objects.hash(injectParentId, injectChildrenId); + return Objects.hash(injectParent, injectChildren); } } From 0afe5afc578cf256544f155c716f4f689c4f5496 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 16 Oct 2024 16:46:41 +0200 Subject: [PATCH 24/34] [frontend/backend] Fixes following PR comments --- .../io/openbas/scheduler/jobs/InjectsExecutionJob.java | 5 ++--- .../repository/InjectDependenciesRepository.java | 10 ---------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index ed7b6bf5f4..9da35b990c 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -244,9 +244,8 @@ private void executeInject(ExecutableInject executableInject) { private Optional> getErrorMessagesPreExecution(String exerciseId, Inject inject) { List injectDependencies = injectDependenciesRepository.findParents(List.of(inject.getId())); if (!injectDependencies.isEmpty()) { - List parents = StreamSupport.stream(injectRepository.findAllById(injectDependencies.stream() - .map(injectDependency -> injectDependency.getCompositeId().getInjectParent().getId()).toList()).spliterator(), false) - .collect(Collectors.toList()); + List parents = injectDependencies.stream() + .map(injectDependency -> injectDependency.getCompositeId().getInjectParent()).toList(); Map mapCondition = getStringBooleanMap(parents, exerciseId, injectDependencies); diff --git a/openbas-model/src/main/java/io/openbas/database/repository/InjectDependenciesRepository.java b/openbas-model/src/main/java/io/openbas/database/repository/InjectDependenciesRepository.java index 2304a35713..87d5ed77bc 100644 --- a/openbas-model/src/main/java/io/openbas/database/repository/InjectDependenciesRepository.java +++ b/openbas-model/src/main/java/io/openbas/database/repository/InjectDependenciesRepository.java @@ -24,14 +24,4 @@ public interface InjectDependenciesRepository extends CrudRepository findParents(@NotNull List childrens); - - @Query(value = "SELECT " + - "inject_parent_id, " + - "inject_children_id, " + - "dependency_condition, " + - "dependency_created_at, " + - "dependency_updated_at " + - "FROM injects_dependencies " + - "WHERE inject_parent_id IN :parents", nativeQuery = true) - List findChildrens(@NotNull List parents); } From 9466c829c879f227749611c6d251a5188737e1eb Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Mon, 21 Oct 2024 11:55:30 +0200 Subject: [PATCH 25/34] [frontend/backend] Chaining injects conditionally (#1385) --- ...191705__Add_table_inject_dependencies.java | 19 +- .../io/openbas/rest/inject/InjectApi.java | 78 ++- .../inject/form/InjectDependencyInput.java | 32 + .../openbas/rest/inject/form/InjectInput.java | 3 +- .../rest/inject/output/InjectOutput.java | 5 +- .../scheduler/jobs/InjectsExecutionJob.java | 23 +- ...jectChainsForm.js => InjectChainsForm.tsx} | 569 ++++++++++-------- .../common/injects/UpdateInjectDetails.js | 5 +- ...hains.js => UpdateInjectLogicalChains.tsx} | 92 +-- .../components/common/chips/ClickableChip.tsx | 2 +- .../common/chips/ClickableModeChip.tsx | 13 +- openbas-front/src/utils/api-types.d.ts | 27 +- .../database/model/InjectDependency.java | 5 +- .../model/InjectDependencyConditions.java | 68 +++ .../database/model/InjectDependencyId.java | 3 + 15 files changed, 594 insertions(+), 350 deletions(-) create mode 100644 openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java rename openbas-front/src/admin/components/common/injects/{InjectChainsForm.js => InjectChainsForm.tsx} (50%) rename openbas-front/src/admin/components/common/injects/{UpdateInjectLogicalChains.js => UpdateInjectLogicalChains.tsx} (62%) create mode 100644 openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java diff --git a/openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java b/openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java index c84971f749..f5bb1703ce 100644 --- a/openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java +++ b/openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java @@ -1,6 +1,9 @@ package io.openbas.migration; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.openbas.database.model.InjectDependencyConditions; import io.openbas.database.model.Variable; +import jakarta.annotation.Resource; import org.flywaydb.core.api.migration.BaseJavaMigration; import org.flywaydb.core.api.migration.Context; import org.springframework.stereotype.Component; @@ -8,18 +11,20 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; +import java.util.List; @Component public class V3_202409191705__Add_table_inject_dependencies extends BaseJavaMigration { @Override public void migrate(Context context) throws Exception { + ObjectMapper mapper = new ObjectMapper(); Statement select = context.getConnection().createStatement(); select.execute(""" CREATE TABLE injects_dependencies ( inject_parent_id VARCHAR(255) NOT NULL REFERENCES injects(inject_id) ON DELETE CASCADE, inject_children_id VARCHAR(255) NOT NULL REFERENCES injects(inject_id) ON DELETE CASCADE, - dependency_condition VARCHAR(255) NOT NULL, + dependency_condition JSONB, dependency_created_at TIMESTAMP DEFAULT now(), dependency_updated_at TIMESTAMP DEFAULT now(), PRIMARY KEY(inject_parent_id, inject_children_id) @@ -32,16 +37,22 @@ PRIMARY KEY(inject_parent_id, inject_children_id) PreparedStatement statement = context.getConnection().prepareStatement( """ INSERT INTO injects_dependencies(inject_parent_id, inject_children_id, dependency_condition) - VALUES (?, ?, ?) + VALUES (?, ?, to_json(?::json)) """ ); while (results.next()) { String injectId = results.getString("inject_id"); String parentId = results.getString("inject_depends_from_another"); - String condition = String.format("%s-Execution-Success == true", parentId); + InjectDependencyConditions.InjectDependencyCondition injectDependencyCondition = new InjectDependencyConditions.InjectDependencyCondition(); + injectDependencyCondition.setMode(InjectDependencyConditions.DependencyMode.and); + InjectDependencyConditions.Condition condition = new InjectDependencyConditions.Condition(); + condition.setKey("Execution"); + condition.setOperator(InjectDependencyConditions.DependencyOperator.eq); + condition.setValue(true); + injectDependencyCondition.setConditions(List.of(condition)); statement.setString(1, parentId); statement.setString(2, injectId); - statement.setString(3, condition); + statement.setString(3, mapper.writeValueAsString(injectDependencyCondition)); statement.addBatch(); } statement.executeBatch(); diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java index 7b447a88da..c64dd0bc4d 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java @@ -299,13 +299,21 @@ public Inject createInjectForExercise(@PathVariable String exerciseId, @Valid @R inject.setExercise(exercise); // Set dependencies if(input.getDependsOn() != null) { - inject.setDependsOn(input.getDependsOn().entrySet().stream().map(entry -> { - InjectDependency injectDependency = new InjectDependency(); - injectDependency.getCompositeId().setInjectChildren(injectRepository.findById(entry.getKey()).orElse(null)); - injectDependency.getCompositeId().setInjectParent(inject); - injectDependency.setCondition(entry.getValue()); - return injectDependency; - }).toList()); + inject.getDependsOn().addAll( + input.getDependsOn() + .stream() + .map(injectDependencyInput -> { + InjectDependency dependency = new InjectDependency(); + InjectDependencyConditions.InjectDependencyCondition injectDependencyCondition = new InjectDependencyConditions.InjectDependencyCondition(); + injectDependencyCondition.setConditions(injectDependencyInput.getConditions()); + injectDependencyCondition.setMode(injectDependencyInput.getMode()); + dependency.setInjectDependencyCondition(injectDependencyCondition); + dependency.setCompositeId(new InjectDependencyId()); + dependency.getCompositeId().setInjectChildren(inject); + dependency.getCompositeId().setInjectParent(injectRepository.findById(injectDependencyInput.getInjectParent()).orElse(null)); + return dependency; + }).toList() + ); } inject.setTeams(fromIterable(teamRepository.findAllById(input.getTeams()))); inject.setAssets(fromIterable(assetService.assets(input.getAssets()))); @@ -477,13 +485,21 @@ public Inject createInjectForScenario( inject.setScenario(scenario); // Set dependencies if(input.getDependsOn() != null) { - inject.setDependsOn(input.getDependsOn().entrySet().stream().map(entry -> { - InjectDependency injectDependency = new InjectDependency(); - injectDependency.getCompositeId().setInjectChildren(injectRepository.findById(entry.getKey()).orElse(null)); - injectDependency.getCompositeId().setInjectParent(inject); - injectDependency.setCondition(entry.getValue()); - return injectDependency; - }).toList()); + inject.getDependsOn().addAll( + input.getDependsOn() + .stream() + .map(injectDependencyInput -> { + InjectDependency dependency = new InjectDependency(); + InjectDependencyConditions.InjectDependencyCondition injectDependencyCondition = new InjectDependencyConditions.InjectDependencyCondition(); + injectDependencyCondition.setConditions(injectDependencyInput.getConditions()); + injectDependencyCondition.setMode(injectDependencyInput.getMode()); + dependency.setInjectDependencyCondition(injectDependencyCondition); + dependency.setCompositeId(new InjectDependencyId()); + dependency.getCompositeId().setInjectChildren(inject); + dependency.getCompositeId().setInjectParent(injectRepository.findById(injectDependencyInput.getInjectParent()).orElse(null)); + return dependency; + }).toList() + ); } inject.setTeams(fromIterable(teamRepository.findAllById(input.getTeams()))); inject.setAssets(fromIterable(assetService.assets(input.getAssets()))); @@ -593,35 +609,39 @@ private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInpu // Set dependencies if(input.getDependsOn() != null) { - input.getDependsOn().entrySet().forEach(entry -> { + input.getDependsOn().forEach(entry -> { Optional existingDependency = inject.getDependsOn().stream() - .filter(injectDependency -> injectDependency.getCompositeId().getInjectParent().getId().equals(entry.getKey())) + .filter(injectDependency -> injectDependency.getCompositeId().getInjectParent().getId().equals(entry.getInjectParent())) .findFirst(); if(existingDependency.isPresent()) { - existingDependency.get().setCondition(entry.getValue()); + existingDependency.get().getInjectDependencyCondition().setConditions(entry.getConditions()); } else { InjectDependency injectDependency = new InjectDependency(); injectDependency.getCompositeId().setInjectChildren(inject); - injectDependency.getCompositeId().setInjectParent(injectRepository.findById(entry.getKey()).orElse(null)); - injectDependency.setCondition(entry.getValue()); + injectDependency.getCompositeId().setInjectParent(injectRepository.findById(entry.getInjectParent()).orElse(null)); + injectDependency.setInjectDependencyCondition(new InjectDependencyConditions.InjectDependencyCondition()); + injectDependency.getInjectDependencyCondition().setConditions(entry.getConditions()); + injectDependency.getInjectDependencyCondition().setMode(entry.getMode()); inject.getDependsOn().add(injectDependency); } }); } List injectDepencyToRemove = new ArrayList<>(); - if(input.getDependsOn() != null && !input.getDependsOn().isEmpty()) { - inject.getDependsOn().forEach( - injectDependency -> { - if (!input.getDependsOn().keySet().contains(injectDependency.getCompositeId().getInjectParent().getId())) { - injectDepencyToRemove.add(injectDependency); + if(inject.getDependsOn() != null && !inject.getDependsOn().isEmpty()) { + if (input.getDependsOn() != null && !input.getDependsOn().isEmpty()) { + inject.getDependsOn().forEach( + injectDependency -> { + if (!input.getDependsOn().stream().map(InjectDependencyInput::getInjectParent).toList().contains(injectDependency.getCompositeId().getInjectParent().getId())) { + injectDepencyToRemove.add(injectDependency); + } } - } - ); - } else { - injectDepencyToRemove.addAll(inject.getDependsOn()); + ); + } else { + injectDepencyToRemove.addAll(inject.getDependsOn()); + } + inject.getDependsOn().removeAll(injectDepencyToRemove); } - inject.getDependsOn().removeAll(injectDepencyToRemove); inject.setTeams(fromIterable(this.teamRepository.findAllById(input.getTeams()))); inject.setAssets(fromIterable(this.assetService.assets(input.getAssets()))); diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java new file mode 100644 index 0000000000..2c644a0080 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java @@ -0,0 +1,32 @@ +package io.openbas.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.openbas.database.model.*; +import jakarta.persistence.EmbeddedId; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Setter +@Getter +public class InjectDependencyInput { + + @JsonProperty("dependency_parent") + private String injectParent; + + @JsonProperty("dependency_mode") + private InjectDependencyConditions.DependencyMode mode; + + @JsonProperty("dependency_conditions") + private List conditions; + + public InjectDependencyConditions.InjectDependencyCondition toInjectDependency() { + InjectDependencyConditions.InjectDependencyCondition injectDependency = new InjectDependencyConditions.InjectDependencyCondition(); + injectDependency.setMode(getMode()); + injectDependency.setConditions(getConditions()); + return injectDependency; + } + +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java index 264c1c770b..ad663f90f1 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectInput.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.node.ObjectNode; import io.openbas.database.model.Inject; +import io.openbas.database.model.InjectDependency; import io.openbas.database.model.InjectorContract; import jakarta.validation.constraints.NotNull; import lombok.Getter; @@ -29,7 +30,7 @@ public class InjectInput { private ObjectNode content; @JsonProperty("inject_depends_on") - private Map dependsOn; + private List dependsOn = new ArrayList<>(); @JsonProperty("inject_depends_duration") private Long dependsDuration; diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java index 573decf044..8f02397141 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/output/InjectOutput.java @@ -42,7 +42,7 @@ public class InjectOutput { private Long dependsDuration; @JsonProperty("inject_depends_on") - private Map dependsOn; + private List dependsOn; @JsonProperty("inject_injector_contract") private InjectorContract injectorContract; @@ -115,8 +115,7 @@ public InjectOutput( this.content = content; if (injectDependency != null) { - this.dependsOn = Stream.of(injectDependency) - .collect(Collectors.toMap(injectDep -> injectDep.getCompositeId().getInjectParent().getId(), InjectDependency::getCondition)); + this.dependsOn = List.of(injectDependency); } } diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index 9da35b990c..f22b433b47 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -8,7 +8,6 @@ import io.openbas.execution.ExecutionExecutorService; import io.openbas.helper.InjectHelper; import io.openbas.service.AtomicTestingService; -import io.openbas.service.ExerciseExpectationService; import jakarta.annotation.Resource; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -30,8 +29,6 @@ import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import static java.time.Instant.now; import static java.util.stream.Collectors.groupingBy; @@ -252,9 +249,8 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I List results = null; for (InjectDependency injectDependency : injectDependencies) { - String expressionToEvaluate = injectDependency.getCondition(); - List conditions = Arrays.stream(injectDependency.getCondition() - .split("(&&|\\|\\|)")).toList(); + String expressionToEvaluate = injectDependency.getInjectDependencyCondition().toString(); + List conditions = injectDependency.getInjectDependencyCondition().getConditions().stream().map(condition -> condition.toString()).toList(); for(String condition : conditions) { expressionToEvaluate = expressionToEvaluate.replaceAll(condition.trim(), condition.trim().replaceAll("(.*-Success)", "#this['$1']")); } @@ -267,7 +263,7 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I results = new ArrayList<>(); results.add("This inject depends on other injects expectations that are not met. The following conditions were not as expected : "); } - results.addAll(labelFromCondition(injectDependency.getCompositeId().getInjectParent(), injectDependency.getCondition())); + results.addAll(labelFromCondition(injectDependency.getCompositeId().getInjectParent(), injectDependency.getInjectDependencyCondition())); } } return results == null ? Optional.empty() : Optional.of(results); @@ -286,9 +282,8 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I Map mapCondition = new HashMap<>(); injectDependencies.forEach(injectDependency -> { - List splittedConditions = List.of(injectDependency.getCondition().split("&&|\\|\\|")); - splittedConditions.forEach(condition -> { - mapCondition.put(condition.split("==")[0].trim(), false); + injectDependency.getInjectDependencyCondition().getConditions().stream().forEach(condition -> { + mapCondition.put(condition.getKey(), false); }); }); @@ -317,12 +312,10 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I return mapCondition; } - private List labelFromCondition(Inject injectParent, String condition) { + private List labelFromCondition(Inject injectParent, InjectDependencyConditions.InjectDependencyCondition condition) { List result = new ArrayList<>(); - List conditionElements = List.of(condition.split("(&&|\\|\\|)")); - for (String conditionElement : conditionElements) { - String type = conditionElement.split("==")[0].trim().replaceAll(injectParent.getId() + "-", "").replaceAll("-Success", ""); - result.add(String.format("Inject '%s' - %s is %s", injectParent.getTitle(), type, conditionElement.endsWith("true") ? "true" : "false")); + for (InjectDependencyConditions.Condition conditionElement : condition.getConditions()) { + result.add(String.format("Inject '%s' - %s is %s", injectParent.getTitle(), conditionElement.getKey(), conditionElement.isValue())); } return result; } diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx similarity index 50% rename from openbas-front/src/admin/components/common/injects/InjectChainsForm.js rename to openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx index 2207ac0eec..fedbfab580 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.js +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx @@ -1,11 +1,30 @@ -import React, { useEffect, useState } from 'react'; +import React, { ReactElement, ReactNode, useEffect, useState } from 'react'; import { makeStyles } from '@mui/styles'; -import { Accordion, AccordionDetails, AccordionSummary, Box, Button, FormControl, IconButton, InputLabel, MenuItem, Select, Tooltip, Typography } from '@mui/material'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Button, + FormControl, + IconButton, + InputLabel, + MenuItem, + Select, + SelectChangeEvent, + Tooltip, + Typography, +} from '@mui/material'; import { Add, DeleteOutlined, ExpandMore } from '@mui/icons-material'; +import { FormApi } from 'final-form'; +import { Value } from 'classnames'; import { useFormatter } from '../../../../components/i18n'; import ClickableModeChip from '../../../../components/common/chips/ClickableModeChip'; import ClickableChip from '../../../../components/common/chips/ClickableChip'; import { capitalize } from '../../../../utils/String'; +import type { InjectDependency } from '../../../../utils/api-types'; +import type { InjectOutputType } from '../../../../actions/injects/Inject'; +import type { Element } from '../../../../components/common/chips/ClickableChip'; const useStyles = makeStyles(() => ({ container: { @@ -22,105 +41,168 @@ const useStyles = makeStyles(() => ({ }, })); -const InjectForm = ({ - values, - form, - injects, -}) => { +interface Props { + values: InjectOutputType & { inject_depends_to: InjectDependency[] }, + form: FormApi, + injects?: InjectOutputType[], +} + +interface ConditionElement { + name: string, + value: boolean, + key: string, + index: number, +} + +interface ConditionType { + parentId?: string, + childrenId?: string, + mode?: string, + conditionElement?: ConditionElement[], +} + +interface Dependency { + inject?: InjectOutputType, + index: number, +} + +const InjectForm: React.FC = ({ values, form, injects }) => { const classes = useStyles(); const { t } = useFormatter(); - const breakpointAndOr = /&&|\|\|/gm; - const breakpointValue = /==/gm; - const typeFromName = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-(.*)-Success/mg; + // List of parents + const [parents, setParents] = useState( + () => { + if (values.inject_depends_on) { + return values.inject_depends_on?.filter((searchInject) => searchInject.dependency_relationship?.inject_children_id === values.inject_id) + .map((inject, index) => { + return { + inject: injects?.find((currentInject) => currentInject.inject_id === inject.dependency_relationship?.inject_parent_id), + index, + }; + }); + } + return []; + }, - const [parents, setParents] = useState( - injects.filter((currentInject) => values.inject_depends_on !== null - && values.inject_depends_on[currentInject.inject_id] !== undefined) - .map((inject, index) => { - return { inject, index }; - }), ); - const [childrens, setChildrens] = useState( - injects.filter((currentInject) => currentInject.inject_depends_on !== null - && currentInject.inject_depends_on[values.inject_id] !== undefined) - .map((inject, index) => { - return { inject, index }; - }), + + // List of childrens + const [childrens, setChildrens] = useState( + () => { + if (values.inject_depends_on) { + return values.inject_depends_on?.filter((searchInject) => searchInject.dependency_relationship?.inject_parent_id === values.inject_id) + .map((inject, index) => { + return { + inject: injects?.find((currentInject) => currentInject.inject_id === inject.dependency_relationship?.inject_children_id), + index, + }; + }); + } + return []; + }, ); + // Property to deactivate the add children button if there are no children available anymore const [addChildrenButtonDisabled, setAddChildrenButtonDisabled] = useState(false); useEffect(() => { - const availableChildrensNumber = injects.filter((currentInject) => currentInject.inject_depends_duration > values.inject_depends_duration).length; - setAddChildrenButtonDisabled(childrens.length >= availableChildrensNumber); + const availableChildrensNumber = injects ? injects.filter((currentInject) => currentInject.inject_depends_duration > values.inject_depends_duration).length : 0; + setAddChildrenButtonDisabled(childrens ? childrens.length >= availableChildrensNumber : true); }, [childrens]); - const getConditionContentParent = (injectDependsOn) => { - const conditions = []; - for (const dependency in injectDependsOn) { - if (Object.hasOwn(injectDependsOn, dependency)) { - const condition = injectDependsOn[dependency]; - const splittedConditions = condition.split(breakpointAndOr); + /** + * Transform an inject dependency into ConditionElement + * @param injectDependsOn an array of injectDependency + */ + const getConditionContentParent = (injectDependsOn: InjectDependency[]) => { + const conditions: ConditionType[] = []; + if (injectDependsOn) { + injectDependsOn.forEach((parent) => { conditions.push({ - parentId: dependency, - mode: condition.includes('||') ? 'or' : 'and', - conditionElement: splittedConditions.map((splitedCondition, index) => { - const key = Array.from(splitedCondition.split(breakpointValue)[0].trim().matchAll(typeFromName), (m) => m[1]); + parentId: parent.dependency_relationship?.inject_parent_id, + mode: parent.dependency_condition?.mode, + conditionElement: parent.dependency_condition?.conditions?.map((dependencyCondition, indexCondition) => { return { - name: splitedCondition.split(breakpointValue)[0].trim(), - value: splitedCondition.split(breakpointValue)[1].trim(), - key: key[0], - index, + name: dependencyCondition.key, + value: dependencyCondition.value!, + key: dependencyCondition.key, + index: indexCondition, }; }), }); - } + }); } return conditions; }; - const getConditionContentChildren = (injectDependsTo) => { - const conditions = []; - for (const children in injectDependsTo) { - if (Object.hasOwn(injectDependsTo, children)) { - for (const dependency in injectDependsTo[children]) { - if (Object.hasOwn(injectDependsTo[children], dependency)) { - const condition = injectDependsTo[children][dependency][values.inject_id]; - if (condition !== undefined) { - const splittedConditions = condition.split(breakpointAndOr); - conditions.push({ - childrenId: dependency, - mode: condition.includes('||') ? 'or' : 'and', - conditionElement: splittedConditions.map((splitedCondition, index) => { - const key = Array.from(splitedCondition.split(breakpointValue)[0].trim().matchAll(typeFromName), (m) => m[1]); - return { - name: splitedCondition.split(breakpointValue)[0].trim(), - value: splitedCondition.split(breakpointValue)[1].trim(), - key: key[0], - index, - }; - }), - }); - } - } - } - } - } + /** + * Transform an inject dependency into ConditionElement + * @param injectDependsTo an array of injectDependency + */ + const getConditionContentChildren = (injectDependsTo: InjectDependency[]) => { + const conditions: ConditionType[] = []; + injectDependsTo.forEach((children) => { + conditions.push({ + childrenId: children.dependency_relationship?.inject_children_id, + mode: children.dependency_condition?.mode, + conditionElement: children.dependency_condition?.conditions?.map((dependencyCondition, indexCondition) => { + return { + name: dependencyCondition.key, + value: dependencyCondition.value!, + key: dependencyCondition.key, + index: indexCondition, + }; + }), + }); + }); return conditions; }; - const [parentConditions, setParentConditions] = useState(getConditionContentParent(values.inject_depends_on)); + const [parentConditions, setParentConditions] = useState(getConditionContentParent(values.inject_depends_on ? values.inject_depends_on : [])); const [childrenConditions, setChildrenConditions] = useState(getConditionContentChildren(values.inject_depends_to)); - const handleChangeParent = (_event, parent) => { + /** + * Handle the change of the parent + * @param _event the event + * @param parent the parent key + */ + const handleChangeParent = (_event: SelectChangeEvent, parent: ReactNode) => { const rx = /\.\$select-parent-(.*)-inject-(.*)/g; - const arr = rx.exec(parent.key); + if (!parent) return; + let key = ''; + const parentElement = parent as ReactElement; + if ('key' in parentElement && parentElement.key !== null) { + key = parentElement.key; + } + if (key === null) { + return; + } + const arr = rx.exec(key); + if (parents === undefined || arr === null || injects === undefined) return; + const newInject = injects.find((currentInject) => currentInject.inject_id === arr[2]); const newParents = parents .map((element) => { if (element.index === parseInt(arr[1], 10)) { + const baseInjectDependency: InjectDependency = { + dependency_relationship: { + inject_parent_id: newInject?.inject_id, + inject_children_id: values.inject_id, + }, + dependency_condition: { + conditions: [ + { + key: 'Execution', + operator: '==', + value: true, + }, + ], + mode: '&&', + }, + }; + newInject!.inject_depends_on = [baseInjectDependency]; return { - inject: injects.find((currentInject) => currentInject.inject_id === arr[2]), + inject: newInject!, index: element.index, }; } @@ -129,34 +211,57 @@ const InjectForm = ({ setParents(newParents); - // We take any parent that is not undefined or undefined (since there is only one parent for now, this'll - // be changed when we allow for multiple parents) - const anyParent = newParents.find((inject) => inject !== undefined); - - const newDependsOn = {}; - newDependsOn[anyParent?.inject.inject_id] = `${anyParent?.inject.inject_id}-Execution-Success == true`; - form.mutators.setValue( 'inject_depends_on', - anyParent?.inject.inject_id ? newDependsOn : null, + newParents, ); - setParentConditions(getConditionContentParent(newDependsOn)); + if (newInject!.inject_depends_on !== null) { + setParentConditions(getConditionContentParent(newInject!.inject_depends_on!)); + } }; const addParent = () => { setParents([...parents, { inject: undefined, index: parents.length }]); }; - const handleChangeChildren = (_event, child) => { + const handleChangeChildren = (_event: SelectChangeEvent, child: ReactNode) => { const rx = /\.\$select-children-(.*)-inject-(.*)/g; - const arr = rx.exec(child.key); + if (!child) return; + let key = ''; + const childElement = (child as ReactElement); + if ('key' in (childElement as ReactElement) && childElement.key !== null) { + key = childElement.key; + } + if (key === null) { + return; + } + const arr = rx.exec(key); + if (childrens === undefined || arr === null || injects === undefined) return; + const newInject = injects.find((currentInject) => currentInject.inject_id === arr[2]); const newChildrens = childrens .map((element) => { if (element.index === parseInt(arr[1], 10)) { + const baseInjectDependency: InjectDependency = { + dependency_relationship: { + inject_parent_id: values.inject_id, + inject_children_id: newInject?.inject_id, + }, + dependency_condition: { + conditions: [ + { + key: 'Execution', + operator: '==', + value: true, + }, + ], + mode: '&&', + }, + }; + newInject!.inject_depends_on = [baseInjectDependency]; return { - inject: injects.find((currentInject) => currentInject.inject_id === arr[2]), + inject: newInject!, index: element.index, }; } @@ -165,25 +270,18 @@ const InjectForm = ({ setChildrens(newChildrens); - const newDependsTo = []; + form.mutators.setValue('inject_depends_to', newChildrens); - for (let i = 0; i < newChildrens.length; i += 1) { - const dependsToChildren = {}; - dependsToChildren[newChildrens[i].inject.inject_id] = {}; - dependsToChildren[newChildrens[i].inject.inject_id][values.inject_id] = `${values.inject_id}-Execution-Success == true`; - newDependsTo.push(dependsToChildren); + if (newInject!.inject_depends_on !== null) { + setChildrenConditions(getConditionContentChildren(newInject!.inject_depends_on!)); } - - form.mutators.setValue('inject_depends_to', newDependsTo); - - setChildrenConditions(getConditionContentChildren(newDependsTo)); }; const addChildren = () => { setChildrens([...childrens, { inject: undefined, index: childrens.length }]); }; - const deleteParent = (parent) => { + const deleteParent = (parent: Dependency) => { const parentIndexInArray = parents.findIndex((currentParent) => currentParent.index === parent.index); if (parentIndexInArray > -1) { @@ -192,16 +290,15 @@ const InjectForm = ({ ...parents.slice(parentIndexInArray + 1), ]; setParents(newParents); - const anyParent = newParents.find((inject) => inject !== undefined); form.mutators.setValue( 'inject_depends_on', - anyParent?.inject.inject_id || null, + newParents, ); } }; - const deleteChildren = (children) => { + const deleteChildren = (children: Dependency) => { const childrenIndexInArray = childrens.findIndex((currentChildren) => currentChildren.index === children.index); if (childrenIndexInArray > -1) { @@ -211,93 +308,59 @@ const InjectForm = ({ ]; setChildrens(newChildrens); - form.mutators.setValue('inject_depends_to', newChildrens.map((inject) => inject.inject?.inject_id)); + form.mutators.setValue('inject_depends_to', newChildrens); } }; - const addConditionParent = (parent) => { - const { mode } = parentConditions.find((currentCondition) => parent.inject.inject_id === currentCondition.parentId); - const newDependsOn = values.inject_depends_on; - newDependsOn[parent.inject.inject_id] = `${values.inject_depends_on[parent.inject.inject_id]} ${mode === 'and' ? '&&' : '||'} ${parent.inject.inject_id}-Execution-Success == true`; + const addConditionParent = (parent: Dependency) => { + const { conditionElement } = parentConditions.find((currentCondition) => parent.inject!.inject_id === currentCondition.parentId)!; + + conditionElement?.push({ + key: 'Execution', + name: 'Execution', + value: true, + index: conditionElement?.length, + }); + form.mutators.setValue( 'inject_depends_on', - newDependsOn, + parents, ); - setParentConditions(getConditionContentParent(newDependsOn)); + setParentConditions(parentConditions); }; - const addConditionChildren = (children) => { - const { mode } = childrenConditions.find((currentCondition) => children.inject.inject_id === currentCondition.childrenId); + const addConditionChildren = (children: Dependency) => { + const { conditionElement } = childrenConditions.find((currentCondition) => children.inject!.inject_id === currentCondition.childrenId)!; - let newDependsTo = {}; - newDependsTo = values.inject_depends_to.map((dependsToChildren) => { - if (dependsToChildren[children.inject.inject_id] !== undefined) { - const newValue = {}; - newValue[children.inject.inject_id] = {}; - newValue[children.inject.inject_id][values.inject_id] = `${dependsToChildren[children.inject.inject_id][values.inject_id]} ${mode === 'and' ? '&&' : '||'} ${values.inject_id}-Execution-Success == true`; - return newValue; - } - return dependsToChildren; + conditionElement?.push({ + key: 'Execution', + name: 'Execution', + value: true, + index: conditionElement?.length, }); + + setChildrenConditions(childrenConditions); form.mutators.setValue( 'inject_depends_to', - newDependsTo, + childrens, ); - setChildrenConditions(getConditionContentChildren(newDependsTo)); }; - const conditionsToStringParent = (conditions) => { - const newConditions = {}; - - for (const dependency in conditions) { - if (Object.hasOwn(conditions, dependency)) { - let writtenCondition = ''; - for (const conditionElementIndex in conditions[dependency].conditionElement) { - if (Object.hasOwn(conditions[dependency].conditionElement, conditionElementIndex)) { - writtenCondition += `${conditionElementIndex > 0 ? ' && ' : ''}${conditions[dependency].conditionElement[conditionElementIndex].name} == ${conditions[dependency].conditionElement[conditionElementIndex].value}`; - } - } - newConditions[conditions[dependency].parentId] = writtenCondition; - } - } - return newConditions; - }; - - const conditionsToStringChildren = (conditions) => { - const newConditions = []; - - for (const dependency in conditions) { - if (Object.hasOwn(conditions, dependency)) { - let writtenCondition = ''; - for (const conditionElementIndex in conditions[dependency].conditionElement) { - if (Object.hasOwn(conditions[dependency].conditionElement, conditionElementIndex)) { - writtenCondition += `${conditionElementIndex > 0 ? '&& ' : ''}${conditions[dependency].conditionElement[conditionElementIndex].name} == ${conditions[dependency].conditionElement[conditionElementIndex].value}`; - } - } - const newCondition = {}; - newCondition[conditions[dependency].childrenId] = {}; - newCondition[conditions[dependency].childrenId][values.inject_id] = writtenCondition; - newConditions.push(newCondition); - } - } - return newConditions; - }; - - const changeParentElement = (newElement, conditions, condition, parent) => { - const newConditionElements = conditions.conditionElement.map((newConditionElement) => { + const changeParentElement = (newElement: Element, conditions: ConditionType, condition: ConditionElement, parent: Dependency) => { + const newConditionElements = conditions.conditionElement?.map((newConditionElement) => { if (newConditionElement.index === condition.index) { return { index: condition.index, key: newElement.key, name: `${conditions.parentId}-${newElement.key}-Success`, - value: newElement.value === 'Success' ? 'true' : 'false', + value: newElement.value === 'Success', }; } return newConditionElement; }); const newParentConditions = parentConditions.map((parentCondition) => { - if (parentCondition.parentId === parent.inject.inject_id) { + if (parentCondition.parentId === parent.inject?.inject_id) { return { ...parentCondition, conditionElement: newConditionElements, @@ -308,24 +371,24 @@ const InjectForm = ({ setParentConditions(newParentConditions); form.mutators.setValue( 'inject_depends_on', - conditionsToStringParent(newParentConditions), + parents, ); }; - const changeChildrenElement = (newElement, conditions, condition, parent) => { - const newConditionElements = conditions.conditionElement.map((newConditionElement) => { + const changeChildrenElement = (newElement: Element, conditions: ConditionType, condition: ConditionElement, parent: Dependency) => { + const newConditionElements = conditions.conditionElement?.map((newConditionElement) => { if (newConditionElement.index === condition.index) { return { index: condition.index, key: newElement.key, name: `${conditions.childrenId}-${newElement.key}-Success`, - value: newElement.value === 'Success' ? 'true' : 'false', + value: newElement.value === 'Success', }; } return newConditionElement; }); const newChildrenConditions = childrenConditions.map((childrenCondition) => { - if (childrenCondition.childrenId === parent.inject.inject_id) { + if (childrenCondition.childrenId === parent.inject?.inject_id) { return { ...childrenCondition, conditionElement: newConditionElements, @@ -336,42 +399,46 @@ const InjectForm = ({ setChildrenConditions(newChildrenConditions); form.mutators.setValue( 'inject_depends_to', - conditionsToStringChildren(newChildrenConditions), + childrens, ); }; - const changeModeParent = (conditions, condition) => { - const newConditionElements = conditions.map((currentCondition) => { + const changeModeParent = (conditions: ConditionType[] | undefined, condition: ConditionType) => { + const newConditionElements = conditions?.map((currentCondition) => { if (currentCondition.parentId === condition.parentId) { return { ...currentCondition, - mode: currentCondition.mode === 'and' ? 'or' : 'and', + mode: currentCondition.mode === '&&' ? '||' : '&&', }; } return currentCondition; }); - setParentConditions(newConditionElements); + if (newConditionElements !== undefined) { + setParentConditions(newConditionElements); + } }; - const changeModeChildren = (conditions, condition) => { - const newConditionElements = conditions.map((currentCondition) => { + const changeModeChildren = (conditions: ConditionType[] | undefined, condition: ConditionType) => { + const newConditionElements = conditions?.map((currentCondition) => { if (currentCondition.childrenId === condition.childrenId) { return { ...currentCondition, - mode: currentCondition.mode === 'and' ? 'or' : 'and', + mode: currentCondition.mode === '&&' ? '||' : '&&', }; } return currentCondition; }); - setChildrenConditions(newConditionElements); + if (newConditionElements !== undefined) { + setChildrenConditions(newConditionElements); + } }; - const deleteConditionParent = (conditions, condition) => { + const deleteConditionParent = (conditions: ConditionType, condition: ConditionElement) => { const newConditionElements = parentConditions.map((currentCondition) => { if (currentCondition.parentId === conditions.parentId) { return { ...currentCondition, - conditionElement: currentCondition.conditionElement.filter((element) => element.index !== condition.index), + conditionElement: currentCondition.conditionElement?.filter((element) => element.index !== condition.index), }; } return currentCondition; @@ -379,16 +446,16 @@ const InjectForm = ({ setParentConditions(newConditionElements); form.mutators.setValue( 'inject_depends_on', - conditionsToStringParent(newConditionElements), + parents, ); }; - const deleteConditionChildren = (conditions, condition) => { + const deleteConditionChildren = (conditions: ConditionType, condition: ConditionElement) => { const newConditionElements = childrenConditions.map((currentCondition) => { if (currentCondition.childrenId === conditions.childrenId) { return { ...currentCondition, - conditionElement: currentCondition.conditionElement.filter((element) => element.index !== condition.index), + conditionElement: currentCondition.conditionElement?.filter((element) => element.index !== condition.index), }; } return currentCondition; @@ -396,35 +463,70 @@ const InjectForm = ({ setChildrenConditions(newConditionElements); form.mutators.setValue( 'inject_depends_to', - conditionsToStringChildren(newConditionElements), + childrens, ); }; - const getAvailableExpectations = (inject) => { - if (inject.inject_content !== null && inject.inject_content !== undefined) { - const expectations = inject.inject_content.expectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); - return ['Execution', ...expectations]; - } if (inject.inject_injector_contract !== undefined - && inject.inject_injector_contract.convertedContent.fields.find((field) => field.key === 'expectations')) { - const expectations = inject.inject_injector_contract.convertedContent.fields - .find((field) => field.key === 'expectations') - .predefinedExpectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); + interface content { + expectations: { + expectation_type: string, + expectation_name: string, + }[] + } + + interface ConvertedContentType { + fields: { + key: string + value: string, + predefinedExpectations: { + expectation_type: string, + expectation_name: string, + }[] + }[], + } + + const getAvailableExpectations = (inject: InjectOutputType | undefined) => { + if (inject?.inject_content !== null && inject?.inject_content !== undefined) { + const expectations = (inject.inject_content as content).expectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); return ['Execution', ...expectations]; } + if (inject?.inject_injector_contract !== undefined + && (inject?.inject_injector_contract.convertedContent as unknown as ConvertedContentType).fields.find((field) => field.key === 'expectations')) { + const predefinedExpectations = (inject.inject_injector_contract.convertedContent as unknown as ConvertedContentType).fields?.find((field) => field.key === 'expectations') + ?.predefinedExpectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); + if (predefinedExpectations !== undefined) { + return ['Execution', ...predefinedExpectations]; + } + } return ['Execution']; }; - const getClickableParentChip = (parent) => { + const canParentAddConditions = (element: Dependency) => { + const expectationsNumber = getAvailableExpectations(element.inject).length; + const parentElement = parentConditions.find((parentCondition) => element.inject !== undefined && parentCondition.parentId === element.inject.inject_id); + if (parentElement === undefined || parentElement.conditionElement === undefined) return true; + + return parentElement?.conditionElement.length < expectationsNumber; + }; + + const canChildrenAddConditions = (element: Dependency) => { + const expectationsNumber = getAvailableExpectations(element.inject).length; + const childrenElement = childrenConditions.find((childrenCondition) => element.inject !== undefined && childrenCondition.childrenId === element.inject.inject_id); + if (childrenElement === undefined || childrenElement.conditionElement === undefined) return true; + + return childrenElement?.conditionElement.length < expectationsNumber; + }; + + const getClickableParentChip = (parent: Dependency) => { const parentChip = parentConditions.find((parentCondition) => parent.inject !== undefined && parentCondition.parentId === parent.inject.inject_id); - if (parentChip === undefined) return (<>); + if (parentChip === undefined || parentChip.conditionElement === undefined) return (<>); return parentChip.conditionElement.map((condition, conditionIndex) => { const conditions = parentConditions .find((parentCondition) => parent.inject !== undefined && parentCondition.parentId === parent.inject.inject_id); - if (conditionIndex < conditions.conditionElement.length - 1) { + if (conditions?.conditionElement !== undefined) { return (<> { changeParentElement(newElement, conditions, condition, parent); }} - /> { changeModeParent(parentConditions, conditions); }} - />); + /> + {conditionIndex < conditions.conditionElement.length - 1 + && { changeModeParent(parentConditions, conditions); }} + /> + }); } - return ( 1 ? () => { deleteConditionParent(conditions, condition); } : undefined - } - onChange={(newElement) => { - changeParentElement(newElement, conditions, condition, parent); - }} - />); + return (<>); }); }; - const getClickableChildrenChip = (children) => { + const getClickableChildrenChip = (children: Dependency) => { const childrenChip = childrenConditions.find((childrenCondition) => children.inject !== undefined && childrenCondition.childrenId === children.inject.inject_id); - if (childrenChip === undefined) return (<>); + if (childrenChip?.conditionElement === undefined) return (<>); return childrenChip .conditionElement.map((condition, conditionIndex) => { const conditions = childrenConditions - .find((childrenCondition) => childrenCondition.childrenId === children.inject.inject_id); - if (conditionIndex < conditions.conditionElement.length - 1) { + .find((childrenCondition) => childrenCondition.childrenId === children.inject?.inject_id); + if (conditions?.conditionElement !== undefined) { return (<> currentInject.inject_id === values.inject_id))} + availableKeys={getAvailableExpectations(injects?.find((currentInject) => currentInject.inject_id === values.inject_id))} availableOperators={['is']} availableValues={['Success', 'Fail']} onDelete={ @@ -480,26 +570,15 @@ const InjectForm = ({ onChange={(newElement) => { changeChildrenElement(newElement, conditions, condition, children); }} - /> { changeModeChildren(childrenConditions, conditions); }} - />); + /> + {conditionIndex < conditions.conditionElement.length - 1 + && { changeModeChildren(childrenConditions, conditions); }} + /> + }); } - return ( currentInject.inject_id === values.inject_id))} - availableOperators={['is']} - availableValues={['Success', 'Fail']} - onDelete={ - conditions.conditionElement.length > 1 ? () => { deleteConditionChildren(conditions, condition); } : undefined - } - onChange={(newElement) => { - changeChildrenElement(newElement, conditions, condition, children); - }} - />); + return (<>); }); }; @@ -514,7 +593,7 @@ const InjectForm = ({ aria-label="Add" size="large" disabled={parents.length > 0 - || injects.filter((currentInject) => currentInject.inject_depends_duration < values.inject_depends_duration).length === 0} + || injects?.filter((currentInject) => currentInject.inject_depends_duration < values.inject_depends_duration).length === 0} onClick={addParent} > @@ -550,11 +629,10 @@ const InjectForm = ({ children.index === childrenSearch.index).inject - ? childrens.find((childrenSearch) => children.index === childrenSearch.index).inject.inject_id : ''} + value={childrens.find((childrenSearch) => children.index === childrenSearch.index)?.inject + ? childrens.find((childrenSearch) => children.index === childrenSearch.index)?.inject?.inject_id : ''} onChange={handleChangeChildren} > - {injects - .filter((currentInject) => currentInject.inject_depends_duration > values.inject_depends_duration + {injects?.filter((currentInject) => currentInject.inject_depends_duration > values.inject_depends_duration && (childrens.find((childrenSearch) => currentInject.inject_id === childrenSearch.inject?.inject_id) === undefined - || childrens.find((childrenSearch) => children.index === childrenSearch.index).inject?.inject_id === currentInject.inject_id)) + || childrens.find((childrenSearch) => children.index === childrenSearch.index)?.inject?.inject_id === currentInject.inject_id)) .map((currentInject) => { return ( { addConditionChildren(children); }} + disabled={!canChildrenAddConditions(children)} style={{ justifyContent: 'start' }} > diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js index 077b7b8aa3..55813d8758 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectDetails.js @@ -18,7 +18,6 @@ import { isEmptyField } from '../../../../utils/utils'; import { tagOptions } from '../../../../utils/Option'; import { splitDuration } from '../../../../utils/Time'; import PlatformIcon from '../../../../components/PlatformIcon'; -import chainingUtils from './chaining/ChainingUtils'; const useStyles = makeStyles((theme) => ({ details: { @@ -210,7 +209,7 @@ const UpdateInjectDetails = ({ inject_asset_groups: assetGroupIds, inject_documents: documents, inject_depends_duration, - inject_depends_on: chainingUtils.fromInjectDependencyToInputDependency(data.inject_depends_on), + inject_depends_on: data.inject_depends_on ? data.inject_depends_on : [], }; await onUpdateInject(values); } @@ -325,7 +324,7 @@ const UpdateInjectDetails = ({ } /> - {tPick(contractContent.label)} + {contractContent !== null ? tPick(contractContent.label) : ''}
({ +const useStyles = makeStyles((theme) => ({ injectorContract: { margin: '10px 0 20px 0', width: '100%', @@ -24,48 +27,53 @@ const useStyles = makeStyles((theme) => ({ }, })); -const UpdateInjectLogicalChains = ({ - inject, - handleClose, - onUpdateInject, - injects, -}) => { +interface Props { + inject: Inject, + handleClose: () => void; + onUpdateInject?: (data: Inject[]) => Promise; + injects?: InjectOutputType[], +} + +interface Dependency { + inject?: InjectOutputType, + index: number, +} + +const UpdateInjectLogicalChains: React.FC = ({ inject, handleClose, onUpdateInject, injects }) => { const { t, tPick } = useFormatter(); const classes = useStyles(); - const injectDependsOn = {}; - if (inject.inject_depends_on !== null) { - for (let i = 0; i < inject.inject_depends_on.length; i += 1) { - injectDependsOn[inject.inject_depends_on[i].dependency_relationship.inject_parent_id] = inject.inject_depends_on[i].dependency_condition; - } - } - const initialValues = { ...inject, - inject_depends_to: injects - .filter((currentInject) => currentInject.inject_depends_on !== null && currentInject.inject_depends_on[inject.inject_id] !== undefined) + inject_depends_to: injects !== undefined ? injects + .filter((currentInject) => currentInject.inject_depends_on !== undefined + && currentInject.inject_depends_on !== null + && currentInject.inject_depends_on + .find((searchInject) => searchInject.dependency_relationship?.inject_parent_id === inject.inject_id) + !== undefined) .map((currentInject) => { - const inject_depends = {}; - inject_depends[currentInject.inject_id] = currentInject.inject_depends_on; - return inject_depends; - }), - inject_depends_on: inject.inject_depends_on !== null ? injectDependsOn : null, + return currentInject.inject_depends_on; + }) : undefined, + inject_depends_on: inject.inject_depends_on, }; - const onSubmit = async (data) => { + const onSubmit = async (data: Inject & { inject_depends_to: Dependency[] }) => { const injectUpdate = { ...data, inject_id: data.inject_id, - inject_injector_contract: data.inject_injector_contract.injector_contract_id, + inject_injector_contract: data.inject_injector_contract?.injector_contract_id, inject_depends_on: data.inject_depends_on, }; - const injectsToUpdate = []; + console.log(data.inject_depends_on); - const childrenIds = data.inject_depends_to.flatMap((childrenInject) => Object.keys(childrenInject)); - const injectsWithoutDependencies = injects + const injectsToUpdate: Inject[] = []; + + const childrenIds = data.inject_depends_to.map((childrenInject: Dependency) => childrenInject.inject?.inject_id); + + const injectsWithoutDependencies = injects ? injects .filter((currentInject) => currentInject.inject_depends_on !== null - && currentInject.inject_depends_on[data.inject_id] !== undefined + && currentInject.inject_depends_on?.find((searchInject) => searchInject.dependency_relationship?.inject_parent_id === data.inject_id) !== undefined && !childrenIds.includes(currentInject.inject_id)) .map((currentInject) => { return { @@ -73,35 +81,37 @@ const UpdateInjectLogicalChains = ({ inject_id: currentInject.inject_id, inject_injector_contract: currentInject.inject_injector_contract.injector_contract_id, inject_depends_on: undefined, - }; - }); + } as unknown as Inject; + }) : []; injectsToUpdate.push(...injectsWithoutDependencies); childrenIds.forEach((childrenId) => { + if (injects === undefined || childrenId === undefined) return; const children = injects.find((currentInject) => currentInject.inject_id === childrenId); if (children !== undefined) { - const injectDependsOnUpdate = {}; - for (let i = 0; i < data.inject_depends_to.length; i += 1) { - if (data.inject_depends_to[i][childrenId] !== undefined) { - injectDependsOnUpdate[inject.inject_id] = data.inject_depends_to[i][childrenId][inject.inject_id]; - } - } + const injectDependsOnUpdate = data.inject_depends_to + .find((dependsTo) => dependsTo.inject?.inject_id === childrenId)?.inject?.inject_depends_on; + console.log(injectDependsOnUpdate); - const injectChildrenUpdate = { + const injectChildrenUpdate: Inject = { ...children, inject_id: children.inject_id, - inject_injector_contract: children.inject_injector_contract.injector_contract_id, + inject_injector_contract: children.inject_injector_contract, inject_depends_on: injectDependsOnUpdate, + inject_created_at: '', + inject_updated_at: '', }; injectsToUpdate.push(injectChildrenUpdate); } }); - await onUpdateInject([injectUpdate, ...injectsToUpdate]); + if (onUpdateInject) { + await onUpdateInject([injectUpdate as Inject, ...injectsToUpdate]); + } handleClose(); }; - const injectorContractContent = JSON.parse(inject.inject_injector_contract.injector_contract_content); + const injectorContractContent = inject.inject_injector_contract?.injector_contract_content ? JSON.parse(inject.inject_injector_contract?.injector_contract_content) : undefined; return ( <> @@ -109,7 +119,7 @@ const UpdateInjectLogicalChains = ({ classes={{ root: classes.injectorContractHeader }} avatar={injectorContractContent?.config?.type ? : } - title={inject?.contract_attack_patterns_external_ids?.join(', ')} + title={inject?.inject_attack_patterns?.map((value) => value.attack_pattern_external_id)?.join(', ')} action={
{inject?.inject_injector_contract?.injector_contract_platforms?.map( (platform) => , @@ -151,7 +161,7 @@ const UpdateInjectLogicalChains = ({ variant="contained" color="secondary" type="submit" - disabled={Object.keys(errors).length > 0 } + disabled={errors !== undefined && Object.keys(errors).length > 0 } > {t('Update')} diff --git a/openbas-front/src/components/common/chips/ClickableChip.tsx b/openbas-front/src/components/common/chips/ClickableChip.tsx index a8ecac70bb..4b15a63ea6 100644 --- a/openbas-front/src/components/common/chips/ClickableChip.tsx +++ b/openbas-front/src/components/common/chips/ClickableChip.tsx @@ -36,7 +36,7 @@ const useStyles = makeStyles((theme: Theme) => ({ }, })); -interface Element { +export interface Element { key: string; operator?: string; value?: string; diff --git a/openbas-front/src/components/common/chips/ClickableModeChip.tsx b/openbas-front/src/components/common/chips/ClickableModeChip.tsx index b726b8c074..2044227634 100644 --- a/openbas-front/src/components/common/chips/ClickableModeChip.tsx +++ b/openbas-front/src/components/common/chips/ClickableModeChip.tsx @@ -24,7 +24,7 @@ const useStyles = makeStyles((theme: Theme) => ({ interface Props { onClick?: () => void; - mode?: 'and' | 'or'; + mode?: string; } const ClickableModeChip: FunctionComponent = ({ @@ -35,6 +35,15 @@ const ClickableModeChip: FunctionComponent = ({ const classes = useStyles(); const { t } = useFormatter(); + const modeToString = () => { + if (mode === '&&') { + return 'AND'; + } if (mode === '||') { + return 'OR'; + } + return mode?.toUpperCase(); + }; + if (!mode) { return <>; } @@ -47,7 +56,7 @@ const ClickableModeChip: FunctionComponent = ({ [classes.hasClickEvent]: !!onClick, })} > - {t(mode.toUpperCase())} + {t(modeToString())}
); }; diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 4be6b8839e..04a13d88f4 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -463,6 +463,12 @@ export interface Communication { listened?: boolean; } +export interface Condition { + key: string; + operator?: "=="; + value?: boolean; +} + export interface CreateUserInput { user_admin?: boolean; user_email: string; @@ -1105,7 +1111,7 @@ export interface Inject { } export interface InjectDependency { - dependency_condition?: string; + dependency_condition?: InjectDependencyCondition; /** @format date-time */ dependency_created_at?: string; dependency_relationship?: InjectDependencyId; @@ -1113,9 +1119,20 @@ export interface InjectDependency { dependency_updated_at?: string; } +export interface InjectDependencyCondition { + conditions?: Condition[]; + mode: "&&" | "||"; +} + export interface InjectDependencyId { - inject_children_id?: Inject; - inject_parent_id?: Inject; + inject_children_id?: string; + inject_parent_id?: string; +} + +export interface InjectDependencyInput { + dependency_conditions?: Condition[]; + dependency_mode?: "&&" | "||"; + dependency_parent?: string; } export interface InjectDocument { @@ -1235,7 +1252,7 @@ export interface InjectInput { inject_country?: string; /** @format int64 */ inject_depends_duration?: number; - inject_depends_on?: Record; + inject_depends_on?: InjectDependencyInput[]; inject_description?: string; inject_documents?: InjectDocumentInput[]; inject_injector_contract?: string; @@ -1253,7 +1270,7 @@ export interface InjectOutput { * @min 0 */ inject_depends_duration: number; - inject_depends_on?: Record; + inject_depends_on?: InjectDependency[]; inject_enabled?: boolean; inject_exercise?: string; inject_id: string; diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependency.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependency.java index fe1c0ba6e7..d7dd0419ec 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectDependency.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependency.java @@ -3,11 +3,13 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.hypersistence.utils.hibernate.type.json.JsonType; import io.openbas.helper.MonoIdDeserializer; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.Type; import org.hibernate.annotations.UpdateTimestamp; import java.time.Instant; @@ -25,7 +27,8 @@ public class InjectDependency { @Column(name = "dependency_condition") @JsonProperty("dependency_condition") - private String condition; + @Type(JsonType.class) + private InjectDependencyConditions.InjectDependencyCondition injectDependencyCondition; @CreationTimestamp @Column(name = "dependency_created_at") diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java new file mode 100644 index 0000000000..669bb900a1 --- /dev/null +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java @@ -0,0 +1,68 @@ +package io.openbas.database.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.apache.logging.log4j.util.Strings; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Optional; + +public class InjectDependencyConditions { + + public enum DependencyMode { + and { + public String toString() { + return "&&"; + } + }, + or { + public String toString() { + return "||"; + } + }; + } + + public enum DependencyOperator { + eq { + public String toString() { + return "=="; + } + }; + } + + @Data + public static class InjectDependencyCondition { + + @NotNull + private DependencyMode mode; // Between filters + private List conditions; + + @Override + public String toString() { + StringBuilder result = new StringBuilder(Strings.EMPTY); + for (var i = 0 ; i < conditions.size() ; i++) { + if(i > 0) { + result.append(mode.toString()); + } + result.append(conditions.get(i).toString()); + } + return result.toString(); + } + } + + @Data + public static class Condition { + + @NotNull + private String key; + private boolean value; + private DependencyOperator operator; + + @Override + public String toString() { + return String.format("%s %s %s", key, operator, value); + } + } +} diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java index f6806f2463..8a2d417192 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java @@ -1,6 +1,7 @@ package io.openbas.database.model; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.openbas.helper.MonoIdDeserializer; import jakarta.persistence.Embeddable; @@ -27,12 +28,14 @@ public class InjectDependencyId implements Serializable { @JsonProperty("inject_parent_id") @JoinColumn(referencedColumnName="inject_id", name="inject_parent_id") @JsonSerialize(using = MonoIdDeserializer.class) + @Schema(type = "string") private Inject injectParent; @ManyToOne @JsonProperty("inject_children_id") @JoinColumn(referencedColumnName="inject_id", name="inject_children_id") @JsonSerialize(using = MonoIdDeserializer.class) + @Schema(type = "string") private Inject injectChildren; @Override From 84325a8bba4b4591b90e6a28c3f8e0b46fee92ac Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Mon, 21 Oct 2024 15:28:36 +0200 Subject: [PATCH 26/34] [frontend/backend] Chaining injects conditionally (#1385) --- .../io/openbas/rest/inject/InjectApi.java | 26 +++++------- .../inject/form/InjectDependencyIdInput.java | 25 ++++++++++++ .../inject/form/InjectDependencyInput.java | 18 ++------- .../common/injects/InjectChainsForm.tsx | 40 ++++++++++--------- .../components/common/injects/Injects.tsx | 4 +- .../injects/UpdateInjectLogicalChains.tsx | 32 +++++++-------- .../common/chips/ClickableModeChip.tsx | 5 --- openbas-front/src/utils/api-types.d.ts | 4 +- .../model/InjectDependencyConditions.java | 19 ++------- .../database/model/InjectDependencyId.java | 1 + 10 files changed, 83 insertions(+), 91 deletions(-) create mode 100644 openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyIdInput.java diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java index c64dd0bc4d..d95e3c2d49 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java @@ -304,13 +304,10 @@ public Inject createInjectForExercise(@PathVariable String exerciseId, @Valid @R .stream() .map(injectDependencyInput -> { InjectDependency dependency = new InjectDependency(); - InjectDependencyConditions.InjectDependencyCondition injectDependencyCondition = new InjectDependencyConditions.InjectDependencyCondition(); - injectDependencyCondition.setConditions(injectDependencyInput.getConditions()); - injectDependencyCondition.setMode(injectDependencyInput.getMode()); - dependency.setInjectDependencyCondition(injectDependencyCondition); + dependency.setInjectDependencyCondition(injectDependencyInput.getConditions()); dependency.setCompositeId(new InjectDependencyId()); dependency.getCompositeId().setInjectChildren(inject); - dependency.getCompositeId().setInjectParent(injectRepository.findById(injectDependencyInput.getInjectParent()).orElse(null)); + dependency.getCompositeId().setInjectParent(injectRepository.findById(injectDependencyInput.getRelationship().getInjectParentId()).orElse(null)); return dependency; }).toList() ); @@ -490,13 +487,10 @@ public Inject createInjectForScenario( .stream() .map(injectDependencyInput -> { InjectDependency dependency = new InjectDependency(); - InjectDependencyConditions.InjectDependencyCondition injectDependencyCondition = new InjectDependencyConditions.InjectDependencyCondition(); - injectDependencyCondition.setConditions(injectDependencyInput.getConditions()); - injectDependencyCondition.setMode(injectDependencyInput.getMode()); - dependency.setInjectDependencyCondition(injectDependencyCondition); + dependency.setInjectDependencyCondition(injectDependencyInput.getConditions()); dependency.setCompositeId(new InjectDependencyId()); dependency.getCompositeId().setInjectChildren(inject); - dependency.getCompositeId().setInjectParent(injectRepository.findById(injectDependencyInput.getInjectParent()).orElse(null)); + dependency.getCompositeId().setInjectParent(injectRepository.findById(injectDependencyInput.getRelationship().getInjectParentId()).orElse(null)); return dependency; }).toList() ); @@ -611,17 +605,17 @@ private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInpu if(input.getDependsOn() != null) { input.getDependsOn().forEach(entry -> { Optional existingDependency = inject.getDependsOn().stream() - .filter(injectDependency -> injectDependency.getCompositeId().getInjectParent().getId().equals(entry.getInjectParent())) + .filter(injectDependency -> injectDependency.getCompositeId().getInjectParent().getId().equals(entry.getRelationship().getInjectParentId())) .findFirst(); if(existingDependency.isPresent()) { - existingDependency.get().getInjectDependencyCondition().setConditions(entry.getConditions()); + existingDependency.get().getInjectDependencyCondition().setConditions(entry.getConditions().getConditions()); } else { InjectDependency injectDependency = new InjectDependency(); injectDependency.getCompositeId().setInjectChildren(inject); - injectDependency.getCompositeId().setInjectParent(injectRepository.findById(entry.getInjectParent()).orElse(null)); + injectDependency.getCompositeId().setInjectParent(injectRepository.findById(entry.getRelationship().getInjectParentId()).orElse(null)); injectDependency.setInjectDependencyCondition(new InjectDependencyConditions.InjectDependencyCondition()); - injectDependency.getInjectDependencyCondition().setConditions(entry.getConditions()); - injectDependency.getInjectDependencyCondition().setMode(entry.getMode()); + injectDependency.getInjectDependencyCondition().setConditions(entry.getConditions().getConditions()); + injectDependency.getInjectDependencyCondition().setMode(entry.getConditions().getMode()); inject.getDependsOn().add(injectDependency); } }); @@ -632,7 +626,7 @@ private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInpu if (input.getDependsOn() != null && !input.getDependsOn().isEmpty()) { inject.getDependsOn().forEach( injectDependency -> { - if (!input.getDependsOn().stream().map(InjectDependencyInput::getInjectParent).toList().contains(injectDependency.getCompositeId().getInjectParent().getId())) { + if (!input.getDependsOn().stream().map((injectDependencyInput -> injectDependencyInput.getRelationship().getInjectParentId())).toList().contains(injectDependency.getCompositeId().getInjectParent().getId())) { injectDepencyToRemove.add(injectDependency); } } diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyIdInput.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyIdInput.java new file mode 100644 index 0000000000..3103102bc3 --- /dev/null +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyIdInput.java @@ -0,0 +1,25 @@ +package io.openbas.rest.inject.form; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.openbas.database.model.Inject; +import io.openbas.database.model.InjectDependencyConditions; +import io.openbas.database.model.InjectDependencyId; +import io.openbas.helper.MonoIdDeserializer; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class InjectDependencyIdInput { + + @JsonProperty("inject_parent_id") + private String injectParentId; + + @JsonProperty("inject_children_id") + private String injectChildrenId; + +} diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java index 2c644a0080..8ce475cb98 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/form/InjectDependencyInput.java @@ -13,20 +13,10 @@ @Getter public class InjectDependencyInput { - @JsonProperty("dependency_parent") - private String injectParent; + @JsonProperty("dependency_relationship") + private InjectDependencyIdInput relationship; - @JsonProperty("dependency_mode") - private InjectDependencyConditions.DependencyMode mode; - - @JsonProperty("dependency_conditions") - private List conditions; - - public InjectDependencyConditions.InjectDependencyCondition toInjectDependency() { - InjectDependencyConditions.InjectDependencyCondition injectDependency = new InjectDependencyConditions.InjectDependencyCondition(); - injectDependency.setMode(getMode()); - injectDependency.setConditions(getConditions()); - return injectDependency; - } + @JsonProperty("dependency_condition") + private InjectDependencyConditions.InjectDependencyCondition conditions; } diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx index fedbfab580..3ad48a364d 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx @@ -22,7 +22,7 @@ import { useFormatter } from '../../../../components/i18n'; import ClickableModeChip from '../../../../components/common/chips/ClickableModeChip'; import ClickableChip from '../../../../components/common/chips/ClickableChip'; import { capitalize } from '../../../../utils/String'; -import type { InjectDependency } from '../../../../utils/api-types'; +import type { Inject, InjectDependency } from '../../../../utils/api-types'; import type { InjectOutputType } from '../../../../actions/injects/Inject'; import type { Element } from '../../../../components/common/chips/ClickableChip'; @@ -42,8 +42,8 @@ const useStyles = makeStyles(() => ({ })); interface Props { - values: InjectOutputType & { inject_depends_to: InjectDependency[] }, - form: FormApi, + values: Inject & { inject_depends_to: InjectDependency[]; }, + form: FormApi>, injects?: InjectOutputType[], } @@ -161,6 +161,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { const [parentConditions, setParentConditions] = useState(getConditionContentParent(values.inject_depends_on ? values.inject_depends_on : [])); const [childrenConditions, setChildrenConditions] = useState(getConditionContentChildren(values.inject_depends_to)); + const injectDependencyFromDependency = (deps: Dependency[]) => { + return deps.flatMap((dependency) => dependency.inject?.inject_depends_on); + }; + /** * Handle the change of the parent * @param _event the event @@ -193,11 +197,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { conditions: [ { key: 'Execution', - operator: '==', + operator: 'eq', value: true, }, ], - mode: '&&', + mode: 'and', }, }; newInject!.inject_depends_on = [baseInjectDependency]; @@ -213,7 +217,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { form.mutators.setValue( 'inject_depends_on', - newParents, + injectDependencyFromDependency(newParents), ); if (newInject!.inject_depends_on !== null) { @@ -252,11 +256,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { conditions: [ { key: 'Execution', - operator: '==', + operator: 'eq', value: true, }, ], - mode: '&&', + mode: 'and', }, }; newInject!.inject_depends_on = [baseInjectDependency]; @@ -270,7 +274,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { setChildrens(newChildrens); - form.mutators.setValue('inject_depends_to', newChildrens); + form.mutators.setValue('inject_depends_to', injectDependencyFromDependency(newChildrens)); if (newInject!.inject_depends_on !== null) { setChildrenConditions(getConditionContentChildren(newInject!.inject_depends_on!)); @@ -293,7 +297,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { form.mutators.setValue( 'inject_depends_on', - newParents, + injectDependencyFromDependency(newParents), ); } }; @@ -324,7 +328,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { form.mutators.setValue( 'inject_depends_on', - parents, + injectDependencyFromDependency(parents), ); setParentConditions(parentConditions); @@ -343,7 +347,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { setChildrenConditions(childrenConditions); form.mutators.setValue( 'inject_depends_to', - childrens, + injectDependencyFromDependency(childrens), ); }; @@ -371,7 +375,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { setParentConditions(newParentConditions); form.mutators.setValue( 'inject_depends_on', - parents, + injectDependencyFromDependency(parents), ); }; @@ -399,7 +403,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { setChildrenConditions(newChildrenConditions); form.mutators.setValue( 'inject_depends_to', - childrens, + injectDependencyFromDependency(childrens), ); }; @@ -408,7 +412,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { if (currentCondition.parentId === condition.parentId) { return { ...currentCondition, - mode: currentCondition.mode === '&&' ? '||' : '&&', + mode: currentCondition.mode === 'and' ? 'or' : 'and', }; } return currentCondition; @@ -423,7 +427,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { if (currentCondition.childrenId === condition.childrenId) { return { ...currentCondition, - mode: currentCondition.mode === '&&' ? '||' : '&&', + mode: currentCondition.mode === 'and' ? 'or' : 'and', }; } return currentCondition; @@ -446,7 +450,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { setParentConditions(newConditionElements); form.mutators.setValue( 'inject_depends_on', - parents, + injectDependencyFromDependency(parents), ); }; @@ -463,7 +467,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { setChildrenConditions(newConditionElements); form.mutators.setValue( 'inject_depends_to', - childrens, + injectDependencyFromDependency(childrens), ); }; diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index cbcece3427..ba649f369e 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -31,7 +31,6 @@ import { useQueryableWithLocalStorage } from '../../../../components/common/quer import ToolBar from '../ToolBar'; import { MESSAGING$ } from '../../../../utils/Environment'; import useEntityToggle from '../../../../utils/hooks/useEntityToggle'; -import chainingUtils from './chaining/ChainingUtils'; const useStyles = makeStyles(() => ({ disabled: { @@ -246,7 +245,7 @@ const Injects: FunctionComponent = ({ const onUpdateInject = async (data: Inject) => { if (selectedInjectId) { await injectContext.onUpdateInject(selectedInjectId, data).then((result: { result: string, entities: { injects: Record } }) => { - onUpdate(chainingUtils.convertInjectStore(result.entities.injects[result.result]) as never); + onUpdate(result); }); } }; @@ -257,7 +256,6 @@ const Injects: FunctionComponent = ({ promises.push(injectContext.onUpdateInject(inject.inject_id, inject).then((result: { result: string, entities: { injects: Record } }) => { if (result.entities) { onUpdate(result); - return chainingUtils.convertInjectStore(result.entities.injects[result.result]) as never; } return undefined; })); diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.tsx b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.tsx index 5ba99874bd..b1906e03bf 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.tsx +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.tsx @@ -8,8 +8,10 @@ import { useFormatter } from '../../../../components/i18n'; import PlatformIcon from '../../../../components/PlatformIcon'; import InjectChainsForm from './InjectChainsForm'; import type { Theme } from '../../../../components/Theme'; -import type { Inject } from '../../../../utils/api-types'; +import type { Inject, InjectDependency } from '../../../../utils/api-types'; import type { InjectOutputType } from '../../../../actions/injects/Inject'; +import { useHelper } from '../../../../store'; +import type { InjectHelper } from '../../../../actions/injects/inject-helper'; const useStyles = makeStyles((theme) => ({ injectorContract: { @@ -34,15 +36,14 @@ interface Props { injects?: InjectOutputType[], } -interface Dependency { - inject?: InjectOutputType, - index: number, -} - const UpdateInjectLogicalChains: React.FC = ({ inject, handleClose, onUpdateInject, injects }) => { const { t, tPick } = useFormatter(); const classes = useStyles(); + const { injectsMap } = useHelper((helper: InjectHelper) => ({ + injectsMap: helper.getInjectsMap(), + })); + const initialValues = { ...inject, inject_depends_to: injects !== undefined ? injects @@ -57,7 +58,7 @@ const UpdateInjectLogicalChains: React.FC = ({ inject, handleClose, onUpd inject_depends_on: inject.inject_depends_on, }; - const onSubmit = async (data: Inject & { inject_depends_to: Dependency[] }) => { + const onSubmit = async (data: Inject & { inject_depends_to: InjectDependency[] }) => { const injectUpdate = { ...data, inject_id: data.inject_id, @@ -65,11 +66,9 @@ const UpdateInjectLogicalChains: React.FC = ({ inject, handleClose, onUpd inject_depends_on: data.inject_depends_on, }; - console.log(data.inject_depends_on); - const injectsToUpdate: Inject[] = []; - const childrenIds = data.inject_depends_to.map((childrenInject: Dependency) => childrenInject.inject?.inject_id); + const childrenIds = data.inject_depends_to.map((childrenInject: InjectDependency) => childrenInject.dependency_relationship?.inject_children_id); const injectsWithoutDependencies = injects ? injects .filter((currentInject) => currentInject.inject_depends_on !== null @@ -77,7 +76,7 @@ const UpdateInjectLogicalChains: React.FC = ({ inject, handleClose, onUpd && !childrenIds.includes(currentInject.inject_id)) .map((currentInject) => { return { - ...currentInject, + ...injectsMap[currentInject.inject_id], inject_id: currentInject.inject_id, inject_injector_contract: currentInject.inject_injector_contract.injector_contract_id, inject_depends_on: undefined, @@ -91,16 +90,13 @@ const UpdateInjectLogicalChains: React.FC = ({ inject, handleClose, onUpd const children = injects.find((currentInject) => currentInject.inject_id === childrenId); if (children !== undefined) { const injectDependsOnUpdate = data.inject_depends_to - .find((dependsTo) => dependsTo.inject?.inject_id === childrenId)?.inject?.inject_depends_on; - console.log(injectDependsOnUpdate); + .find((dependsTo) => dependsTo.dependency_relationship?.inject_children_id === childrenId); const injectChildrenUpdate: Inject = { - ...children, + ...injectsMap[children.inject_id], inject_id: children.inject_id, - inject_injector_contract: children.inject_injector_contract, - inject_depends_on: injectDependsOnUpdate, - inject_created_at: '', - inject_updated_at: '', + inject_injector_contract: children.inject_injector_contract.injector_contract_id, + inject_depends_on: injectDependsOnUpdate ? [injectDependsOnUpdate] : [], }; injectsToUpdate.push(injectChildrenUpdate); } diff --git a/openbas-front/src/components/common/chips/ClickableModeChip.tsx b/openbas-front/src/components/common/chips/ClickableModeChip.tsx index 2044227634..540c6d3d68 100644 --- a/openbas-front/src/components/common/chips/ClickableModeChip.tsx +++ b/openbas-front/src/components/common/chips/ClickableModeChip.tsx @@ -36,11 +36,6 @@ const ClickableModeChip: FunctionComponent = ({ const { t } = useFormatter(); const modeToString = () => { - if (mode === '&&') { - return 'AND'; - } if (mode === '||') { - return 'OR'; - } return mode?.toUpperCase(); }; diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 04a13d88f4..865bd00048 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -465,7 +465,7 @@ export interface Communication { export interface Condition { key: string; - operator?: "=="; + operator: "eq"; value?: boolean; } @@ -1121,7 +1121,7 @@ export interface InjectDependency { export interface InjectDependencyCondition { conditions?: Condition[]; - mode: "&&" | "||"; + mode: "and" | "or"; } export interface InjectDependencyId { diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java index 669bb900a1..7bdb719a2c 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java @@ -12,24 +12,12 @@ public class InjectDependencyConditions { public enum DependencyMode { - and { - public String toString() { - return "&&"; - } - }, - or { - public String toString() { - return "||"; - } - }; + and, + or; } public enum DependencyOperator { - eq { - public String toString() { - return "=="; - } - }; + eq; } @Data @@ -58,6 +46,7 @@ public static class Condition { @NotNull private String key; private boolean value; + @NotNull private DependencyOperator operator; @Override diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java index 8a2d417192..8391832777 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyId.java @@ -1,6 +1,7 @@ package io.openbas.database.model; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import io.swagger.v3.oas.annotations.media.Schema; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.openbas.helper.MonoIdDeserializer; From 8bd93e29db4a5a85e9bcd52eb8f8169464997ca7 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Mon, 21 Oct 2024 15:38:33 +0200 Subject: [PATCH 27/34] [frontend/backend] Chaining injects conditionally (#1385) --- .../common/injects/chaining/ChainingUtils.tsx | 33 ------------------- .../src/components/ChainedTimeline.tsx | 7 ++-- 2 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx diff --git a/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx b/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx deleted file mode 100644 index 197f679c75..0000000000 --- a/openbas-front/src/admin/components/common/injects/chaining/ChainingUtils.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { InjectDependency } from '../../../../../utils/api-types'; -import type { InjectStore } from '../../../../../actions/injects/Inject'; - -const fromInjectDependencyToInputDependency = (dependencies: InjectDependency[]) => { - const result : Record = {}; - for (let i = 0; i < dependencies.length; i += 1) { - if (dependencies[i].dependency_relationship?.inject_parent_id !== undefined - && dependencies[i].dependency_condition !== undefined) { - const key = dependencies[i].dependency_relationship!.inject_parent_id; - if (key !== undefined) { - result[key as unknown as string] = dependencies[i].dependency_condition!; - } - } - } - return dependencies.length > 0 ? result : null; -}; - -const convertInjectStore = (injectStore: InjectStore) => { - const dependingOn : Record = {}; - injectStore.inject_depends_on?.forEach((value) => { - if (value.dependency_condition != null && value.dependency_relationship?.inject_parent_id !== undefined) { - dependingOn[value.dependency_relationship?.inject_parent_id as unknown as string] = value.dependency_condition; - } - }); - const newResult = { - ...injectStore, - inject_depends_on: Object.keys(dependingOn).length === 0 ? null : dependingOn, - }; - - return newResult; -}; - -export default { fromInjectDependencyToInputDependency, convertInjectStore }; diff --git a/openbas-front/src/components/ChainedTimeline.tsx b/openbas-front/src/components/ChainedTimeline.tsx index 5a7d2b726a..f37d1bfb37 100644 --- a/openbas-front/src/components/ChainedTimeline.tsx +++ b/openbas-front/src/components/ChainedTimeline.tsx @@ -37,7 +37,6 @@ import { useFormatter } from './i18n'; import type { AssetGroupsHelper } from '../actions/asset_groups/assetgroup-helper'; import type { EndpointHelper } from '../actions/assets/asset-helper'; import type { Inject } from '../utils/api-types'; -import chainingUtils from '../admin/components/common/injects/chaining/ChainingUtils'; const useStyles = makeStyles(() => ({ container: { @@ -288,7 +287,7 @@ const ChainedTimelineFlow: FunctionComponent = ({ inject_id: node.id, inject_depends_duration: convertCoordinatesToTime(node.position), inject_depends_on: injectFromMap.inject_depends_on !== null - ? chainingUtils.fromInjectDependencyToInputDependency(injectFromMap.inject_depends_on) : null, + ? injectFromMap.inject_depends_on : null, }; onUpdateInject([inject]); setCurrentUpdatedNode(node); @@ -348,11 +347,11 @@ const ChainedTimelineFlow: FunctionComponent = ({ const { position } = node; const { data } = node; const dependsOn = nodes.find((currentNode) => (data.inject?.inject_depends_on !== null - && data.inject?.inject_depends_on![currentNode.id])); + && data.inject?.inject_depends_on!.find((value) => value.dependency_relationship?.inject_children_id === currentNode.id))); const dependsTo = nodes .filter((currentNode) => (currentNode.data.inject?.inject_depends_on !== undefined && currentNode.data.inject?.inject_depends_on !== null - && currentNode.data.inject?.inject_depends_on[node.id] !== undefined)) + && currentNode.data.inject?.inject_depends_on.find((value) => value.dependency_relationship?.inject_parent_id === node.id) !== undefined)) .sort((a, b) => a.data.inject!.inject_depends_duration - b.data.inject!.inject_depends_duration)[0]; const aSecond = gapSize / (minutesPerGapAllowed[minutesPerGapIndex] * 60); if (dependsOn?.position && position.x <= dependsOn?.position.x) { From 1daa8a567e08ae50c8882c1677727a424a89952b Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Tue, 22 Oct 2024 00:34:35 +0200 Subject: [PATCH 28/34] [frontend/backend] Chaining injects conditionally (#1385) --- .../io/openbas/rest/inject/InjectApi.java | 1 + openbas-front/src/actions/injects/Inject.d.ts | 37 ++ .../common/injects/InjectChainsForm.tsx | 362 ++++++++++-------- .../components/common/injects/Injects.tsx | 4 +- .../injects/UpdateInjectLogicalChains.tsx | 2 +- .../src/components/ChainedTimeline.tsx | 72 +++- .../components/common/chips/ClickableChip.tsx | 2 +- .../model/InjectDependencyConditions.java | 21 +- 8 files changed, 325 insertions(+), 176 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java index d95e3c2d49..ef1acdf524 100644 --- a/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java +++ b/openbas-api/src/main/java/io/openbas/rest/inject/InjectApi.java @@ -609,6 +609,7 @@ private Inject updateInject(@NotBlank final String injectId, @NotNull InjectInpu .findFirst(); if(existingDependency.isPresent()) { existingDependency.get().getInjectDependencyCondition().setConditions(entry.getConditions().getConditions()); + existingDependency.get().getInjectDependencyCondition().setMode(entry.getConditions().getMode()); } else { InjectDependency injectDependency = new InjectDependency(); injectDependency.getCompositeId().setInjectChildren(inject); diff --git a/openbas-front/src/actions/injects/Inject.d.ts b/openbas-front/src/actions/injects/Inject.d.ts index 9d70a9a315..f1b3154851 100644 --- a/openbas-front/src/actions/injects/Inject.d.ts +++ b/openbas-front/src/actions/injects/Inject.d.ts @@ -39,3 +39,40 @@ export type InjectExpectationStore = Omit ({ @@ -47,25 +47,6 @@ interface Props { injects?: InjectOutputType[], } -interface ConditionElement { - name: string, - value: boolean, - key: string, - index: number, -} - -interface ConditionType { - parentId?: string, - childrenId?: string, - mode?: string, - conditionElement?: ConditionElement[], -} - -interface Dependency { - inject?: InjectOutputType, - index: number, -} - const InjectForm: React.FC = ({ values, form, injects }) => { const classes = useStyles(); const { t } = useFormatter(); @@ -90,11 +71,15 @@ const InjectForm: React.FC = ({ values, form, injects }) => { // List of childrens const [childrens, setChildrens] = useState( () => { - if (values.inject_depends_on) { - return values.inject_depends_on?.filter((searchInject) => searchInject.dependency_relationship?.inject_parent_id === values.inject_id) + if (injects !== undefined) { + return injects?.filter( + (searchInject) => searchInject.inject_depends_on?.find( + (dependsOnSearch) => dependsOnSearch.dependency_relationship?.inject_parent_id === values.inject_id, + ) !== undefined, + ) .map((inject, index) => { return { - inject: injects?.find((currentInject) => currentInject.inject_id === inject.dependency_relationship?.inject_children_id), + inject, index, }; }); @@ -118,18 +103,21 @@ const InjectForm: React.FC = ({ values, form, injects }) => { const conditions: ConditionType[] = []; if (injectDependsOn) { injectDependsOn.forEach((parent) => { - conditions.push({ - parentId: parent.dependency_relationship?.inject_parent_id, - mode: parent.dependency_condition?.mode, - conditionElement: parent.dependency_condition?.conditions?.map((dependencyCondition, indexCondition) => { - return { - name: dependencyCondition.key, - value: dependencyCondition.value!, - key: dependencyCondition.key, - index: indexCondition, - }; - }), - }); + if (parent !== null) { + conditions.push({ + parentId: parent.dependency_relationship?.inject_parent_id, + childrenId: parent.dependency_relationship?.inject_children_id, + mode: parent.dependency_condition?.mode, + conditionElement: parent.dependency_condition?.conditions?.map((dependencyCondition, indexCondition) => { + return { + name: dependencyCondition.key, + value: dependencyCondition.value!, + key: dependencyCondition.key, + index: indexCondition, + }; + }), + }); + } }); } return conditions; @@ -139,21 +127,25 @@ const InjectForm: React.FC = ({ values, form, injects }) => { * Transform an inject dependency into ConditionElement * @param injectDependsTo an array of injectDependency */ - const getConditionContentChildren = (injectDependsTo: InjectDependency[]) => { + const getConditionContentChildren = (injectDependsTo: (InjectDependency | undefined)[]) => { const conditions: ConditionType[] = []; + console.log(injectDependsTo); injectDependsTo.forEach((children) => { - conditions.push({ - childrenId: children.dependency_relationship?.inject_children_id, - mode: children.dependency_condition?.mode, - conditionElement: children.dependency_condition?.conditions?.map((dependencyCondition, indexCondition) => { - return { - name: dependencyCondition.key, - value: dependencyCondition.value!, - key: dependencyCondition.key, - index: indexCondition, - }; - }), - }); + if (children !== undefined) { + conditions.push({ + parentId: values.inject_id, + childrenId: children.dependency_relationship?.inject_children_id, + mode: children.dependency_condition?.mode, + conditionElement: children.dependency_condition?.conditions?.map((dependencyCondition, indexCondition) => { + return { + name: dependencyCondition.key, + value: dependencyCondition.value!, + key: dependencyCondition.key, + index: indexCondition, + }; + }), + }); + } }); return conditions; }; @@ -162,7 +154,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { const [childrenConditions, setChildrenConditions] = useState(getConditionContentChildren(values.inject_depends_to)); const injectDependencyFromDependency = (deps: Dependency[]) => { - return deps.flatMap((dependency) => dependency.inject?.inject_depends_on); + return deps.flatMap((dependency) => (dependency.inject?.inject_depends_on !== null ? dependency.inject?.inject_depends_on : [])); }; /** @@ -274,10 +266,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { setChildrens(newChildrens); - form.mutators.setValue('inject_depends_to', injectDependencyFromDependency(newChildrens)); + const dependsTo = injectDependencyFromDependency(newChildrens); + form.mutators.setValue('inject_depends_to', dependsTo); if (newInject!.inject_depends_on !== null) { - setChildrenConditions(getConditionContentChildren(newInject!.inject_depends_on!)); + setChildrenConditions(getConditionContentChildren(dependsTo.filter((dep) => dep !== undefined))); } }; @@ -316,39 +309,98 @@ const InjectForm: React.FC = ({ values, form, injects }) => { } }; - const addConditionParent = (parent: Dependency) => { - const { conditionElement } = parentConditions.find((currentCondition) => parent.inject!.inject_id === currentCondition.parentId)!; + const updateDependsOn = (conditions: ConditionType) => { + const result: InjectDependency = { + dependency_relationship: { + inject_parent_id: conditions.parentId, + inject_children_id: conditions.childrenId, + }, + dependency_condition: { + mode: conditions.mode === 'and' ? 'and' : 'or', + conditions: conditions.conditionElement?.map((value) => { + return { + value: value.value, + key: value.key, + operator: 'eq', + }; + }), + }, + }; + return result; + }; - conditionElement?.push({ - key: 'Execution', - name: 'Execution', - value: true, - index: conditionElement?.length, - }); + const getAvailableExpectations = (inject: InjectOutputType | undefined) => { + if (inject?.inject_content !== null && inject?.inject_content !== undefined) { + const expectations = (inject.inject_content as Content).expectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); + return ['Execution', ...expectations]; + } + if (inject?.inject_injector_contract !== undefined + && (inject?.inject_injector_contract.convertedContent as unknown as ConvertedContentType).fields.find((field) => field.key === 'expectations')) { + const predefinedExpectations = (inject.inject_injector_contract.convertedContent as unknown as ConvertedContentType).fields?.find((field) => field.key === 'expectations') + ?.predefinedExpectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); + if (predefinedExpectations !== undefined) { + return ['Execution', ...predefinedExpectations]; + } + } + return ['Execution']; + }; - form.mutators.setValue( - 'inject_depends_on', - injectDependencyFromDependency(parents), - ); + const addConditionParent = (parent: Dependency) => { + const currentConditions = parentConditions.find((currentCondition) => parent.inject!.inject_id === currentCondition.parentId); + + if (parent.inject !== undefined && currentConditions !== undefined) { + const updatedParent = parents.find((currentParent) => currentParent.inject?.inject_id === parent.inject?.inject_id); + let expectationString = 'Execution'; + if (currentConditions?.conditionElement !== undefined) { + expectationString = getAvailableExpectations(parent.inject) + .find((expectation) => !currentConditions?.conditionElement?.find((conditionElement) => conditionElement.key === expectation)); + } + currentConditions.conditionElement?.push({ + key: expectationString, + name: expectationString, + value: true, + index: currentConditions.conditionElement?.length, + }); + + if (updatedParent?.inject?.inject_depends_on !== undefined) { + updatedParent.inject.inject_depends_on = [updateDependsOn(currentConditions)]; + } - setParentConditions(parentConditions); + setParentConditions(parentConditions); + form.mutators.setValue( + 'inject_depends_on', + injectDependencyFromDependency(parents), + ); + } }; const addConditionChildren = (children: Dependency) => { - const { conditionElement } = childrenConditions.find((currentCondition) => children.inject!.inject_id === currentCondition.childrenId)!; + const currentConditions = childrenConditions.find((currentCondition) => children.inject!.inject_id === currentCondition.childrenId); + + if (children.inject !== undefined && currentConditions !== undefined) { + const updatedChildren = childrens.find((currentChildren) => currentChildren.inject?.inject_id === children.inject?.inject_id); + let expectationString = 'Execution'; + if (currentConditions?.conditionElement !== undefined) { + expectationString = getAvailableExpectations(values as InjectOutput as InjectOutputType) + .find((expectation) => !currentConditions?.conditionElement?.find((conditionElement) => conditionElement.key === expectation)); + } + currentConditions.conditionElement?.push({ + key: expectationString, + name: expectationString, + value: true, + index: currentConditions.conditionElement?.length, + }); - conditionElement?.push({ - key: 'Execution', - name: 'Execution', - value: true, - index: conditionElement?.length, - }); + if (updatedChildren?.inject?.inject_depends_on !== undefined) { + updatedChildren.inject.inject_depends_on = [updateDependsOn(currentConditions)]; + } - setChildrenConditions(childrenConditions); - form.mutators.setValue( - 'inject_depends_to', - injectDependencyFromDependency(childrens), - ); + setChildrenConditions(childrenConditions); + form.mutators.setValue( + 'inject_depends_to', + injectDependencyFromDependency(childrens), + ); + } }; const changeParentElement = (newElement: Element, conditions: ConditionType, condition: ConditionElement, parent: Dependency) => { @@ -373,13 +425,19 @@ const InjectForm: React.FC = ({ values, form, injects }) => { return parentCondition; }); setParentConditions(newParentConditions); + + const updatedParent = parents.find((currentParent) => currentParent.inject?.inject_id === parent.inject?.inject_id); + const newCondition = newParentConditions.find((parentCondition) => parentCondition.parentId === parent.inject?.inject_id); + if (updatedParent?.inject?.inject_depends_on !== undefined && newCondition !== undefined) { + updatedParent.inject.inject_depends_on = [updateDependsOn(newCondition)]; + } form.mutators.setValue( 'inject_depends_on', injectDependencyFromDependency(parents), ); }; - const changeChildrenElement = (newElement: Element, conditions: ConditionType, condition: ConditionElement, parent: Dependency) => { + const changeChildrenElement = (newElement: Element, conditions: ConditionType, condition: ConditionElement, children: Dependency) => { const newConditionElements = conditions.conditionElement?.map((newConditionElement) => { if (newConditionElement.index === condition.index) { return { @@ -392,7 +450,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { return newConditionElement; }); const newChildrenConditions = childrenConditions.map((childrenCondition) => { - if (childrenCondition.childrenId === parent.inject?.inject_id) { + if (childrenCondition.childrenId === children.inject?.inject_id) { return { ...childrenCondition, conditionElement: newConditionElements, @@ -401,6 +459,12 @@ const InjectForm: React.FC = ({ values, form, injects }) => { return childrenCondition; }); setChildrenConditions(newChildrenConditions); + + const updatedChildren = childrens.find((currentChildren) => currentChildren.inject?.inject_id === children.inject?.inject_id); + const newCondition = newChildrenConditions.find((childrenCondition) => childrenCondition.childrenId === children.inject?.inject_id); + if (updatedChildren?.inject?.inject_depends_on !== undefined && newCondition !== undefined) { + updatedChildren.inject.inject_depends_on = [updateDependsOn(newCondition)]; + } form.mutators.setValue( 'inject_depends_to', injectDependencyFromDependency(childrens), @@ -420,6 +484,16 @@ const InjectForm: React.FC = ({ values, form, injects }) => { if (newConditionElements !== undefined) { setParentConditions(newConditionElements); } + + const newCurrentCondition = newConditionElements?.find((currentCondition) => currentCondition.parentId === condition.parentId); + const updatedParent = parents.find((currentParent) => currentParent.inject?.inject_id === newCurrentCondition?.parentId); + if (updatedParent?.inject?.inject_depends_on !== undefined && newCurrentCondition !== undefined) { + updatedParent.inject.inject_depends_on = [updateDependsOn(newCurrentCondition)]; + } + form.mutators.setValue( + 'inject_depends_on', + injectDependencyFromDependency(parents), + ); }; const changeModeChildren = (conditions: ConditionType[] | undefined, condition: ConditionType) => { @@ -435,6 +509,16 @@ const InjectForm: React.FC = ({ values, form, injects }) => { if (newConditionElements !== undefined) { setChildrenConditions(newConditionElements); } + + const newCurrentCondition = newConditionElements?.find((currentCondition) => currentCondition.childrenId === condition.childrenId); + const updatedChildren = childrens.find((currentChildren) => currentChildren.inject?.inject_id === newCurrentCondition?.childrenId); + if (updatedChildren?.inject?.inject_depends_on !== undefined && newCurrentCondition !== undefined) { + updatedChildren.inject.inject_depends_on = [updateDependsOn(newCurrentCondition)]; + } + form.mutators.setValue( + 'inject_depends_to', + injectDependencyFromDependency(childrens), + ); }; const deleteConditionParent = (conditions: ConditionType, condition: ConditionElement) => { @@ -448,6 +532,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { return currentCondition; }); setParentConditions(newConditionElements); + + const updatedParent = parents.find((currentParent) => currentParent.inject?.inject_id === conditions?.parentId); + if (updatedParent?.inject?.inject_depends_on !== undefined && conditions !== undefined) { + updatedParent.inject.inject_depends_on = [updateDependsOn(conditions)]; + } form.mutators.setValue( 'inject_depends_on', injectDependencyFromDependency(parents), @@ -465,60 +554,22 @@ const InjectForm: React.FC = ({ values, form, injects }) => { return currentCondition; }); setChildrenConditions(newConditionElements); + + const updatedChildren = childrens.find((currentChildren) => currentChildren.inject?.inject_id === conditions.childrenId); + if (updatedChildren?.inject?.inject_depends_on !== undefined && conditions !== undefined) { + updatedChildren.inject.inject_depends_on = [updateDependsOn(conditions)]; + } form.mutators.setValue( 'inject_depends_to', injectDependencyFromDependency(childrens), ); }; - interface content { - expectations: { - expectation_type: string, - expectation_name: string, - }[] - } - - interface ConvertedContentType { - fields: { - key: string - value: string, - predefinedExpectations: { - expectation_type: string, - expectation_name: string, - }[] - }[], - } - - const getAvailableExpectations = (inject: InjectOutputType | undefined) => { - if (inject?.inject_content !== null && inject?.inject_content !== undefined) { - const expectations = (inject.inject_content as content).expectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); - return ['Execution', ...expectations]; - } - if (inject?.inject_injector_contract !== undefined - && (inject?.inject_injector_contract.convertedContent as unknown as ConvertedContentType).fields.find((field) => field.key === 'expectations')) { - const predefinedExpectations = (inject.inject_injector_contract.convertedContent as unknown as ConvertedContentType).fields?.find((field) => field.key === 'expectations') - ?.predefinedExpectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); - if (predefinedExpectations !== undefined) { - return ['Execution', ...predefinedExpectations]; - } - } - return ['Execution']; - }; - - const canParentAddConditions = (element: Dependency) => { - const expectationsNumber = getAvailableExpectations(element.inject).length; - const parentElement = parentConditions.find((parentCondition) => element.inject !== undefined && parentCondition.parentId === element.inject.inject_id); - if (parentElement === undefined || parentElement.conditionElement === undefined) return true; - - return parentElement?.conditionElement.length < expectationsNumber; - }; - - const canChildrenAddConditions = (element: Dependency) => { - const expectationsNumber = getAvailableExpectations(element.inject).length; - const childrenElement = childrenConditions.find((childrenCondition) => element.inject !== undefined && childrenCondition.childrenId === element.inject.inject_id); - if (childrenElement === undefined || childrenElement.conditionElement === undefined) return true; + const canAddConditions = (inject: InjectOutputType, conditions?: ConditionType) => { + const expectationsNumber = getAvailableExpectations(inject).length; + if (conditions === undefined || conditions.conditionElement === undefined) return true; - return childrenElement?.conditionElement.length < expectationsNumber; + return conditions?.conditionElement.length < expectationsNumber; }; const getClickableParentChip = (parent: Dependency) => { @@ -528,26 +579,26 @@ const InjectForm: React.FC = ({ values, form, injects }) => { const conditions = parentConditions .find((parentCondition) => parent.inject !== undefined && parentCondition.parentId === parent.inject.inject_id); if (conditions?.conditionElement !== undefined) { - return (<> + 1 ? () => { deleteConditionParent(conditions, condition); } : undefined } - onChange={(newElement) => { - changeParentElement(newElement, conditions, condition, parent); - }} - /> + onChange={(newElement) => { + changeParentElement(newElement, conditions, condition, parent); + }} + /> {conditionIndex < conditions.conditionElement.length - 1 && { changeModeParent(parentConditions, conditions); }} /> - }); + }); } return (<>); }); @@ -561,26 +612,26 @@ const InjectForm: React.FC = ({ values, form, injects }) => { const conditions = childrenConditions .find((childrenCondition) => childrenCondition.childrenId === children.inject?.inject_id); if (conditions?.conditionElement !== undefined) { - return (<> currentInject.inject_id === values.inject_id))} - availableOperators={['is']} - availableValues={['Success', 'Fail']} - onDelete={ + return (
+ currentInject.inject_id === values.inject_id))} + availableOperators={['is']} + availableValues={['Success', 'Fail']} + onDelete={ conditions.conditionElement.length > 1 ? () => { deleteConditionChildren(conditions, condition); } : undefined } - onChange={(newElement) => { - changeChildrenElement(newElement, conditions, condition, children); - }} - /> + onChange={(newElement) => { + changeChildrenElement(newElement, conditions, condition, children); + }} + /> {conditionIndex < conditions.conditionElement.length - 1 && { changeModeChildren(childrenConditions, conditions); }} /> - }); + }
); } return (<>); }); @@ -667,7 +718,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { addConditionParent(parent); }} style={{ justifyContent: 'start' }} - disabled={!canParentAddConditions(parent)} + disabled={!canAddConditions(parent.inject!, parentConditions.find((parentCondition) => parentCondition.parentId === parent.inject?.inject_id))} > @@ -760,7 +811,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { onClick={() => { addConditionChildren(children); }} - disabled={!canChildrenAddConditions(children)} + disabled={!canAddConditions( + values as InjectOutput as InjectOutputType, + childrenConditions.find((childrenCondition) => childrenCondition.childrenId === children.inject?.inject_id), + )} style={{ justifyContent: 'start' }} > diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx index ba649f369e..4a0d359c65 100644 --- a/openbas-front/src/admin/components/common/injects/Injects.tsx +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -242,10 +242,12 @@ const Injects: FunctionComponent = ({ onCreate(result); }); }; + const onUpdateInject = async (data: Inject) => { if (selectedInjectId) { await injectContext.onUpdateInject(selectedInjectId, data).then((result: { result: string, entities: { injects: Record } }) => { onUpdate(result); + return result; }); } }; @@ -255,7 +257,7 @@ const Injects: FunctionComponent = ({ data.forEach((inject) => { promises.push(injectContext.onUpdateInject(inject.inject_id, inject).then((result: { result: string, entities: { injects: Record } }) => { if (result.entities) { - onUpdate(result); + return result.entities.injects[result.result]; } return undefined; })); diff --git a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.tsx b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.tsx index b1906e03bf..f3e5845eaa 100644 --- a/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.tsx +++ b/openbas-front/src/admin/components/common/injects/UpdateInjectLogicalChains.tsx @@ -52,7 +52,7 @@ const UpdateInjectLogicalChains: React.FC = ({ inject, handleClose, onUpd && currentInject.inject_depends_on .find((searchInject) => searchInject.dependency_relationship?.inject_parent_id === inject.inject_id) !== undefined) - .map((currentInject) => { + .flatMap((currentInject) => { return currentInject.inject_depends_on; }) : undefined, inject_depends_on: inject.inject_depends_on, diff --git a/openbas-front/src/components/ChainedTimeline.tsx b/openbas-front/src/components/ChainedTimeline.tsx index f37d1bfb37..24d895b724 100644 --- a/openbas-front/src/components/ChainedTimeline.tsx +++ b/openbas-front/src/components/ChainedTimeline.tsx @@ -36,7 +36,7 @@ import NodePhantom from './nodes/NodePhantom'; import { useFormatter } from './i18n'; import type { AssetGroupsHelper } from '../actions/asset_groups/assetgroup-helper'; import type { EndpointHelper } from '../actions/assets/asset-helper'; -import type { Inject } from '../utils/api-types'; +import type { Inject, InjectDependency } from '../utils/api-types'; const useStyles = makeStyles(() => ({ container: { @@ -177,17 +177,17 @@ const ChainedTimelineFlow: FunctionComponent = ({ }; const updateEdges = () => { - const newEdges = injects.filter((inject) => inject.inject_depends_on != null && inject.inject_depends_on !== undefined) + const newEdges = injects.filter((inject) => inject.inject_depends_on !== null && inject.inject_depends_on !== undefined) .flatMap((inject) => { const results = []; if (inject.inject_depends_on !== undefined) { - for (const key of Object.keys(inject.inject_depends_on)) { + for (let i = 0; i < inject.inject_depends_on.length; i += 1) { results.push({ - id: `${inject.inject_id}->${key}`, + id: `${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}->${inject.inject_id}`, target: `${inject.inject_id}`, targetHandle: `target-${inject.inject_id}`, - source: `${key}`, - sourceHandle: `source-${key}`, + source: `${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}`, + sourceHandle: `source-${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}`, label: '', labelShowBg: false, labelStyle: { fill: theme.palette.text?.primary, fontSize: 9 }, @@ -325,13 +325,27 @@ const ChainedTimelineFlow: FunctionComponent = ({ const inject = injects.find((currentInject) => currentInject.inject_id === connection.target); const injectParent = injects.find((currentInject) => currentInject.inject_id === connection.source); if (inject !== undefined && injectParent !== undefined && inject.inject_depends_duration > injectParent.inject_depends_duration) { - const newDependsOn: Record = {}; - newDependsOn[injectParent?.inject_id] = `${injectParent?.inject_id}-Execution-Success == true`; + const newDependsOn: InjectDependency = { + dependency_relationship: { + inject_children_id: inject.inject_id, + inject_parent_id: injectParent.inject_id, + }, + dependency_condition: + { + mode: 'and', + conditions: [ + { + key: 'Execution', operator: 'eq', value: true, + }, + ], + }, + }; + const injectToUpdate = { ...injectsMap[inject.inject_id], inject_injector_contract: inject.inject_injector_contract.injector_contract_id, inject_id: inject.inject_id, - inject_depends_on: newDependsOn, + inject_depends_on: [newDependsOn], }; onUpdateInject([injectToUpdate]); } @@ -347,7 +361,7 @@ const ChainedTimelineFlow: FunctionComponent = ({ const { position } = node; const { data } = node; const dependsOn = nodes.find((currentNode) => (data.inject?.inject_depends_on !== null - && data.inject?.inject_depends_on!.find((value) => value.dependency_relationship?.inject_children_id === currentNode.id))); + && data.inject?.inject_depends_on!.find((value) => value.dependency_relationship?.inject_parent_id === currentNode.id))); const dependsTo = nodes .filter((currentNode) => (currentNode.data.inject?.inject_depends_on !== undefined && currentNode.data.inject?.inject_depends_on !== null @@ -477,13 +491,26 @@ const ChainedTimelineFlow: FunctionComponent = ({ inject_depends_on: undefined, }; updates.push(injectToRemoveEdge); - const newDependsOn: Record = {}; - newDependsOn[edge.source] = `${edge.source}-Execution-Success == true`; + const newDependsOn: InjectDependency = { + dependency_relationship: { + inject_children_id: injectToUpdate.inject_id, + inject_parent_id: edge.source, + }, + dependency_condition: + { + mode: 'and', + conditions: [ + { + key: 'Execution', operator: 'eq', value: true, + }, + ], + }, + }; const injectToUpdateEdge = { ...injectsMap[injectToUpdate.inject_id], inject_injector_contract: injectToUpdate.inject_injector_contract.injector_contract_id, inject_id: injectToUpdate.inject_id, - inject_depends_on: newDependsOn, + inject_depends_on: [newDependsOn], }; updates.push(injectToUpdateEdge); onUpdateInject(updates); @@ -492,13 +519,26 @@ const ChainedTimelineFlow: FunctionComponent = ({ const inject = injects.find((currentInject) => currentInject.inject_id === edge.target); const parent = injects.find((currentInject) => currentInject.inject_id === connectionState.toNode?.id); if (inject !== undefined && parent !== undefined && parent.inject_depends_duration < inject.inject_depends_duration) { - const newDependsOn: Record = {}; - newDependsOn[connectionState.toNode!.id] = `${connectionState.toNode?.id}-Execution-Success == true`; + const newDependsOn: InjectDependency = { + dependency_relationship: { + inject_children_id: inject.inject_id, + inject_parent_id: connectionState.toNode?.id, + }, + dependency_condition: + { + mode: 'and', + conditions: [ + { + key: 'Execution', operator: 'eq', value: true, + }, + ], + }, + }; const injectToUpdate = { ...injectsMap[inject.inject_id], inject_injector_contract: inject.inject_injector_contract.injector_contract_id, inject_id: inject.inject_id, - inject_depends_on: connectionState.toNode?.id, + inject_depends_on: [newDependsOn], }; onUpdateInject([injectToUpdate]); } diff --git a/openbas-front/src/components/common/chips/ClickableChip.tsx b/openbas-front/src/components/common/chips/ClickableChip.tsx index 4b15a63ea6..8cdd60f489 100644 --- a/openbas-front/src/components/common/chips/ClickableChip.tsx +++ b/openbas-front/src/components/common/chips/ClickableChip.tsx @@ -119,7 +119,7 @@ const ClickableChip: FunctionComponent = ({ {t('OR')} ; } - return (<>{or} {o}); + return (
{or} {o}
); }); } return ( {t('undefined')}); diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java index 7bdb719a2c..2ffcaf803f 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java @@ -12,12 +12,27 @@ public class InjectDependencyConditions { public enum DependencyMode { - and, - or; + and{ + @Override + public String toString() { + return "&&"; + } + }, + or{ + @Override + public String toString() { + return "||"; + } + }; } public enum DependencyOperator { - eq; + eq { + @Override + public String toString() { + return "=="; + } + }; } @Data From f1ccba597f45c64520aafc8b7b447d86d3f675b2 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Tue, 22 Oct 2024 00:58:04 +0200 Subject: [PATCH 29/34] [frontend/backend] Chaining injects conditionally (#1385) --- .../io/openbas/scheduler/jobs/InjectsExecutionJob.java | 10 +++++----- .../components/common/injects/InjectChainsForm.tsx | 1 - .../database/model/InjectDependencyConditions.java | 5 ++++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index f22b433b47..f30934e842 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -250,9 +250,9 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I for (InjectDependency injectDependency : injectDependencies) { String expressionToEvaluate = injectDependency.getInjectDependencyCondition().toString(); - List conditions = injectDependency.getInjectDependencyCondition().getConditions().stream().map(condition -> condition.toString()).toList(); + List conditions = injectDependency.getInjectDependencyCondition().getConditions().stream().map(InjectDependencyConditions.Condition::toString).toList(); for(String condition : conditions) { - expressionToEvaluate = expressionToEvaluate.replaceAll(condition.trim(), condition.trim().replaceAll("(.*-Success)", "#this['$1']")); + expressionToEvaluate = expressionToEvaluate.replaceAll(condition.split("==")[0].trim(), String.format("#this['%s']", condition.split("==")[0].trim())); } ExpressionParser parser = new SpelExpressionParser(); @@ -288,16 +288,16 @@ private Optional> getErrorMessagesPreExecution(String exerciseId, I }); parents.forEach(parent -> { - mapCondition.put(parent.getId() + "-Execution-Success", + mapCondition.put("Execution", parent.getStatus().isPresent() && !parent.getStatus().get().getName().equals(ExecutionStatus.ERROR) && !executionStatusesNotReady.contains(parent.getStatus().get().getName())); List expectations = injectExpectationRepository.findAllForExerciseAndInject(exerciseId, parent.getId()); expectations.forEach(injectExpectation -> { - String name = String.format("%s-%s-Success", parent.getId(), StringUtils.capitalize(injectExpectation.getType().toString().toLowerCase())); + String name = StringUtils.capitalize(injectExpectation.getType().toString().toLowerCase()); if(injectExpectation.getType().equals(InjectExpectation.EXPECTATION_TYPE.MANUAL)) { - name = String.format("%s-%s-Success", parent.getId(), injectExpectation.getName()); + name = injectExpectation.getName(); } if(InjectExpectation.EXPECTATION_TYPE.CHALLENGE.equals(injectExpectation.getType()) || InjectExpectation.EXPECTATION_TYPE.ARTICLE.equals(injectExpectation.getType())) { diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx index 9ccec32905..dd19d0530f 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx @@ -129,7 +129,6 @@ const InjectForm: React.FC = ({ values, form, injects }) => { */ const getConditionContentChildren = (injectDependsTo: (InjectDependency | undefined)[]) => { const conditions: ConditionType[] = []; - console.log(injectDependsTo); injectDependsTo.forEach((children) => { if (children !== undefined) { conditions.push({ diff --git a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java index 2ffcaf803f..4652a74f5f 100644 --- a/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java +++ b/openbas-model/src/main/java/io/openbas/database/model/InjectDependencyConditions.java @@ -3,6 +3,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.util.Strings; import javax.annotation.Nullable; @@ -48,10 +49,12 @@ public String toString() { for (var i = 0 ; i < conditions.size() ; i++) { if(i > 0) { result.append(mode.toString()); + result.append(StringUtils.SPACE); } result.append(conditions.get(i).toString()); + result.append(StringUtils.SPACE); } - return result.toString(); + return result.toString().trim(); } } From b651335b5a6e1796d7fb5cdf9c4558e6b20e2b31 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Tue, 22 Oct 2024 01:52:51 +0200 Subject: [PATCH 30/34] [frontend/backend] Chaining injects conditionally (#1385) --- .../service/ScenarioToExerciseService.java | 9 +++++++- .../common/injects/InjectChainsForm.tsx | 11 +++++++-- .../src/components/ChainedTimeline.tsx | 23 +++++++++++-------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java b/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java index 91165b891e..c933c5b67a 100644 --- a/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java +++ b/openbas-api/src/main/java/io/openbas/service/ScenarioToExerciseService.java @@ -248,7 +248,14 @@ public Exercise toExercise( if(scenarioInject.getDependsOn() != null) { Inject injectToUpdate = mapExerciseInjectsByScenarioInject.get(scenarioInject.getId()); injectToUpdate.getDependsOn().clear(); - injectToUpdate.getDependsOn().addAll(scenarioInject.getDependsOn()); + injectToUpdate.getDependsOn().addAll(scenarioInject.getDependsOn().stream().map((injectDependency -> { + InjectDependency dep = new InjectDependency(); + dep.setCompositeId(injectDependency.getCompositeId()); + dep.setInjectDependencyCondition(injectDependency.getInjectDependencyCondition()); + dep.getCompositeId().setInjectParent(mapExerciseInjectsByScenarioInject.get(dep.getCompositeId().getInjectParent().getId())); + dep.getCompositeId().setInjectChildren(injectToUpdate); + return dep; + })).toList()); this.injectRepository.save(injectToUpdate); } }); diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx index dd19d0530f..88eb243582 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx @@ -179,6 +179,13 @@ const InjectForm: React.FC = ({ values, form, injects }) => { const newParents = parents .map((element) => { if (element.index === parseInt(arr[1], 10)) { + const previousInject = injects.find((value) => value.inject_id === element.inject?.inject_id); + if (previousInject?.inject_depends_on !== undefined) { + previousInject!.inject_depends_on = previousInject!.inject_depends_on?.filter( + (dependsOn) => dependsOn.dependency_relationship?.inject_children_id !== values.inject_id, + ); + } + const baseInjectDependency: InjectDependency = { dependency_relationship: { inject_parent_id: newInject?.inject_id, @@ -295,7 +302,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { }; const deleteChildren = (children: Dependency) => { - const childrenIndexInArray = childrens.findIndex((currentChildren) => currentChildren.index === children.index); + const childrenIndexInArray = childrens.findIndex((currentChildren) => currentChildren.inject?.inject_id === children.inject?.inject_id); if (childrenIndexInArray > -1) { const newChildrens = [ @@ -304,7 +311,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { ]; setChildrens(newChildrens); - form.mutators.setValue('inject_depends_to', newChildrens); + form.mutators.setValue('inject_depends_to', injectDependencyFromDependency(newChildrens)); } }; diff --git a/openbas-front/src/components/ChainedTimeline.tsx b/openbas-front/src/components/ChainedTimeline.tsx index 24d895b724..694bf94ffc 100644 --- a/openbas-front/src/components/ChainedTimeline.tsx +++ b/openbas-front/src/components/ChainedTimeline.tsx @@ -182,20 +182,23 @@ const ChainedTimelineFlow: FunctionComponent = ({ const results = []; if (inject.inject_depends_on !== undefined) { for (let i = 0; i < inject.inject_depends_on.length; i += 1) { - results.push({ - id: `${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}->${inject.inject_id}`, - target: `${inject.inject_id}`, - targetHandle: `target-${inject.inject_id}`, - source: `${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}`, - sourceHandle: `source-${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}`, - label: '', - labelShowBg: false, - labelStyle: { fill: theme.palette.text?.primary, fontSize: 9 }, - }); + if (inject.inject_depends_on[i].dependency_relationship?.inject_children_id === inject.inject_id) { + results.push({ + id: `${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}->${inject.inject_depends_on[i].dependency_relationship?.inject_children_id}`, + target: `${inject.inject_depends_on[i].dependency_relationship?.inject_children_id}`, + targetHandle: `target-${inject.inject_depends_on[i].dependency_relationship?.inject_children_id}`, + source: `${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}`, + sourceHandle: `source-${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}`, + label: '', + labelShowBg: false, + labelStyle: { fill: theme.palette.text?.primary, fontSize: 9 }, + }); + } } } return results; }); + setEdges(newEdges); }; From fca864230a8d5e194ec5ef20a02ef0a9dbd12346 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Tue, 22 Oct 2024 02:05:18 +0200 Subject: [PATCH 31/34] [frontend/backend] Chaining injects conditionally (#1385) --- ...dencies.java => V3_46__Add_table_inject_dependencies.java} | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) rename openbas-api/src/main/java/io/openbas/migration/{V3_202409191705__Add_table_inject_dependencies.java => V3_46__Add_table_inject_dependencies.java} (94%) diff --git a/openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java b/openbas-api/src/main/java/io/openbas/migration/V3_46__Add_table_inject_dependencies.java similarity index 94% rename from openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java rename to openbas-api/src/main/java/io/openbas/migration/V3_46__Add_table_inject_dependencies.java index f5bb1703ce..21ca0f6e99 100644 --- a/openbas-api/src/main/java/io/openbas/migration/V3_202409191705__Add_table_inject_dependencies.java +++ b/openbas-api/src/main/java/io/openbas/migration/V3_46__Add_table_inject_dependencies.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.openbas.database.model.InjectDependencyConditions; -import io.openbas.database.model.Variable; -import jakarta.annotation.Resource; import org.flywaydb.core.api.migration.BaseJavaMigration; import org.flywaydb.core.api.migration.Context; import org.springframework.stereotype.Component; @@ -14,7 +12,7 @@ import java.util.List; @Component -public class V3_202409191705__Add_table_inject_dependencies extends BaseJavaMigration { +public class V3_46__Add_table_inject_dependencies extends BaseJavaMigration { @Override public void migrate(Context context) throws Exception { From 2c74d81de6c4acf64390dd6c99b76a0810c92cb4 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Tue, 22 Oct 2024 12:33:47 +0200 Subject: [PATCH 32/34] [frontend] Chaining Injects UI improvements (#1385) --- .../common/injects/InjectChainsForm.tsx | 86 +++++++++++++++++++ .../common/chips/ClickableModeChip.tsx | 6 +- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx index 88eb243582..2593f0ccbf 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx @@ -152,6 +152,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { const [parentConditions, setParentConditions] = useState(getConditionContentParent(values.inject_depends_on ? values.inject_depends_on : [])); const [childrenConditions, setChildrenConditions] = useState(getConditionContentChildren(values.inject_depends_to)); + /** + * Get the inject dependency object from dependency ones + * @param deps the inject depencies + */ const injectDependencyFromDependency = (deps: Dependency[]) => { return deps.flatMap((dependency) => (dependency.inject?.inject_depends_on !== null ? dependency.inject?.inject_depends_on : [])); }; @@ -223,10 +227,18 @@ const InjectForm: React.FC = ({ values, form, injects }) => { } }; + /** + * Add a new parent inject + */ const addParent = () => { setParents([...parents, { inject: undefined, index: parents.length }]); }; + /** + * Handle the change of a children + * @param _event + * @param child + */ const handleChangeChildren = (_event: SelectChangeEvent, child: ReactNode) => { const rx = /\.\$select-children-(.*)-inject-(.*)/g; if (!child) return; @@ -280,10 +292,17 @@ const InjectForm: React.FC = ({ values, form, injects }) => { } }; + /** + * Add a new children inject + */ const addChildren = () => { setChildrens([...childrens, { inject: undefined, index: childrens.length }]); }; + /** + * Delete a parent inject + * @param parent + */ const deleteParent = (parent: Dependency) => { const parentIndexInArray = parents.findIndex((currentParent) => currentParent.index === parent.index); @@ -301,6 +320,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { } }; + /** + * Delete a children inject + * @param children + */ const deleteChildren = (children: Dependency) => { const childrenIndexInArray = childrens.findIndex((currentChildren) => currentChildren.inject?.inject_id === children.inject?.inject_id); @@ -315,6 +338,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { } }; + /** + * Returns an updated depends on from a ConditionType + * @param conditions + */ const updateDependsOn = (conditions: ConditionType) => { const result: InjectDependency = { dependency_relationship: { @@ -335,6 +362,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { return result; }; + /** + * Get the list of available expectations + * @param inject + */ const getAvailableExpectations = (inject: InjectOutputType | undefined) => { if (inject?.inject_content !== null && inject?.inject_content !== undefined) { const expectations = (inject.inject_content as Content).expectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); @@ -351,6 +382,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { return ['Execution']; }; + /** + * Add a new condition to a parent inject + * @param parent + */ const addConditionParent = (parent: Dependency) => { const currentConditions = parentConditions.find((currentCondition) => parent.inject!.inject_id === currentCondition.parentId); @@ -380,6 +415,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { } }; + /** + * Add a new condition to a children inject + * @param children + */ const addConditionChildren = (children: Dependency) => { const currentConditions = childrenConditions.find((currentCondition) => children.inject!.inject_id === currentCondition.childrenId); @@ -409,6 +448,13 @@ const InjectForm: React.FC = ({ values, form, injects }) => { } }; + /** + * Handle a change in a condition of a parent element + * @param newElement + * @param conditions + * @param condition + * @param parent + */ const changeParentElement = (newElement: Element, conditions: ConditionType, condition: ConditionElement, parent: Dependency) => { const newConditionElements = conditions.conditionElement?.map((newConditionElement) => { if (newConditionElement.index === condition.index) { @@ -443,6 +489,13 @@ const InjectForm: React.FC = ({ values, form, injects }) => { ); }; + /** + * Handle a change in a condition of a children element + * @param newElement + * @param conditions + * @param condition + * @param children + */ const changeChildrenElement = (newElement: Element, conditions: ConditionType, condition: ConditionElement, children: Dependency) => { const newConditionElements = conditions.conditionElement?.map((newConditionElement) => { if (newConditionElement.index === condition.index) { @@ -477,6 +530,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { ); }; + /** + * Changes the mode (AND/OR) in a parent inject + * @param conditions + * @param condition + */ const changeModeParent = (conditions: ConditionType[] | undefined, condition: ConditionType) => { const newConditionElements = conditions?.map((currentCondition) => { if (currentCondition.parentId === condition.parentId) { @@ -502,6 +560,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { ); }; + /** + * Changes the mode (AND/OR) in a children inject + * @param conditions + * @param condition + */ const changeModeChildren = (conditions: ConditionType[] | undefined, condition: ConditionType) => { const newConditionElements = conditions?.map((currentCondition) => { if (currentCondition.childrenId === condition.childrenId) { @@ -527,6 +590,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { ); }; + /** + * Delete a condition from a parent inject + * @param conditions + * @param condition + */ const deleteConditionParent = (conditions: ConditionType, condition: ConditionElement) => { const newConditionElements = parentConditions.map((currentCondition) => { if (currentCondition.parentId === conditions.parentId) { @@ -549,6 +617,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { ); }; + /** + * Delete a condition from a children inject + * @param conditions + * @param condition + */ const deleteConditionChildren = (conditions: ConditionType, condition: ConditionElement) => { const newConditionElements = childrenConditions.map((currentCondition) => { if (currentCondition.childrenId === conditions.childrenId) { @@ -571,6 +644,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { ); }; + /** + * Whether or not we can add a new condition + * @param inject + * @param conditions + */ const canAddConditions = (inject: InjectOutputType, conditions?: ConditionType) => { const expectationsNumber = getAvailableExpectations(inject).length; if (conditions === undefined || conditions.conditionElement === undefined) return true; @@ -578,6 +656,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { return conditions?.conditionElement.length < expectationsNumber; }; + /** + * Return a clickable parent chip + * @param parent + */ const getClickableParentChip = (parent: Dependency) => { const parentChip = parentConditions.find((parentCondition) => parent.inject !== undefined && parentCondition.parentId === parent.inject.inject_id); if (parentChip === undefined || parentChip.conditionElement === undefined) return (<>); @@ -610,6 +692,10 @@ const InjectForm: React.FC = ({ values, form, injects }) => { }); }; + /** + * Return a clickable children chip + * @param parent + */ const getClickableChildrenChip = (children: Dependency) => { const childrenChip = childrenConditions.find((childrenCondition) => children.inject !== undefined && childrenCondition.childrenId === children.inject.inject_id); if (childrenChip?.conditionElement === undefined) return (<>); diff --git a/openbas-front/src/components/common/chips/ClickableModeChip.tsx b/openbas-front/src/components/common/chips/ClickableModeChip.tsx index 540c6d3d68..d0e3d5a79f 100644 --- a/openbas-front/src/components/common/chips/ClickableModeChip.tsx +++ b/openbas-front/src/components/common/chips/ClickableModeChip.tsx @@ -35,10 +35,6 @@ const ClickableModeChip: FunctionComponent = ({ const classes = useStyles(); const { t } = useFormatter(); - const modeToString = () => { - return mode?.toUpperCase(); - }; - if (!mode) { return <>; } @@ -51,7 +47,7 @@ const ClickableModeChip: FunctionComponent = ({ [classes.hasClickEvent]: !!onClick, })} > - {t(modeToString())} + {t(mode.toUpperCase())} ); }; From 79faac7cc3d6b04a049c4508111987d9a87adb9f Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Tue, 22 Oct 2024 14:41:40 +0200 Subject: [PATCH 33/34] [frontend] Chaining Injects conditionally (#1385) --- .../assets/asset_groups/AssetGroups.tsx | 4 +- .../common/queryable/filter/FilterChips.tsx | 4 +- .../queryable/filter/FilterModeChip.tsx | 55 ------------------- .../pagination/PaginationComponentV2.tsx | 4 +- 4 files changed, 6 insertions(+), 61 deletions(-) delete mode 100644 openbas-front/src/components/common/queryable/filter/FilterModeChip.tsx diff --git a/openbas-front/src/admin/components/assets/asset_groups/AssetGroups.tsx b/openbas-front/src/admin/components/assets/asset_groups/AssetGroups.tsx index 352f69fc23..3c1530e947 100644 --- a/openbas-front/src/admin/components/assets/asset_groups/AssetGroups.tsx +++ b/openbas-front/src/admin/components/assets/asset_groups/AssetGroups.tsx @@ -25,7 +25,7 @@ import ExportButton from '../../../../components/common/ExportButton'; import { useQueryableWithLocalStorage } from '../../../../components/common/queryable/useQueryableWithLocalStorage'; import SortHeadersComponentV2 from '../../../../components/common/queryable/sort/SortHeadersComponentV2'; import { Header } from '../../../../components/common/SortHeadersList'; -import FilterModeChip from '../../../../components/common/queryable/filter/FilterModeChip'; +import ClickableModeChip from '../../../../components/common/chips/ClickableModeChip'; import FilterChipValues from '../../../../components/common/queryable/filter/FilterChipValues'; const useStyles = makeStyles(() => ({ @@ -77,7 +77,7 @@ const computeRuleValues = (assetGroup: AssetGroupOutput, t: (value: string) => s <> {assetGroup.asset_group_dynamic_filter.filters.map((filter, idx) => ( - {idx !== 0 && } + {idx !== 0 && } = ({ } return ( - {idx !== 0 && } + {idx !== 0 && } ({ - mode: { - borderRadius: 4, - fontFamily: 'Consolas, monaco, monospace', - backgroundColor: theme.palette.action?.selected, - padding: '0 8px', - display: 'flex', - alignItems: 'center', - }, - hasClickEvent: { - cursor: 'pointer', - '&:hover': { - backgroundColor: theme.palette.action?.disabled, - textDecorationLine: 'underline', - }, - }, -})); - -interface Props { - onClick?: () => void; - mode?: 'and' | 'or'; -} - -const FilterModeChip: FunctionComponent = ({ - onClick, - mode, -}) => { - // Standard hooks - const classes = useStyles(); - const { t } = useFormatter(); - - if (!mode) { - return <>; - } - - return ( -
- {t(mode.toUpperCase())} -
- ); -}; - -export default FilterModeChip; diff --git a/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx b/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx index f73ce4edfa..c78dd20338 100644 --- a/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx +++ b/openbas-front/src/components/common/queryable/pagination/PaginationComponentV2.tsx @@ -14,7 +14,7 @@ import TextSearchComponent from '../textSearch/TextSearchComponent'; import FilterAutocomplete, { OptionPropertySchema } from '../filter/FilterAutocomplete'; import useFilterableProperties from '../filter/useFilterableProperties'; import FilterChips from '../filter/FilterChips'; -import FilterModeChip from '../filter/FilterModeChip'; +import ClickableModeChip from '../../chips/ClickableModeChip'; import InjectorContractSwitchFilter from '../../../../admin/components/common/filters/InjectorContractSwitchFilter'; import TablePaginationComponentV2 from './TablePaginationComponentV2'; @@ -184,7 +184,7 @@ const PaginationComponentV2 = ({ onDelete={() => queryableHelpers.filterHelpers.handleRemoveFilterByKey(MITRE_FILTER_KEY)} /> {(searchPaginationInput.filterGroup?.filters?.filter((f) => availableFilterNames?.filter((n) => n !== MITRE_FILTER_KEY).includes(f.key)).length ?? 0) > 0 && ( - From 5db2b2d37113c852f06d582509fedafba70c2d39 Mon Sep 17 00:00:00 2001 From: Gael Leblan Date: Wed, 23 Oct 2024 11:03:47 +0200 Subject: [PATCH 34/34] [frontend] Chaining injects logically (#1385) --- .../scheduler/jobs/InjectsExecutionJob.java | 7 +- .../common/injects/InjectChainsForm.tsx | 182 ++++++++++++------ .../src/components/ChainedTimeline.tsx | 117 +++++++++-- .../common/chaining/ChainingUtils.tsx | 14 ++ .../src/components/nodes/NodeInject.tsx | 6 +- 5 files changed, 245 insertions(+), 81 deletions(-) create mode 100644 openbas-front/src/components/common/chaining/ChainingUtils.tsx diff --git a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java index f30934e842..30a7d8c6fc 100644 --- a/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java +++ b/openbas-api/src/main/java/io/openbas/scheduler/jobs/InjectsExecutionJob.java @@ -156,8 +156,11 @@ private void executeInject(ExecutableInject executableInject) { Inject inject = executableInject.getInjection().getInject(); // We are now checking if we depend on another inject and if it did not failed - Optional> errorMessages = getErrorMessagesPreExecution(executableInject.getExercise().getId(), inject); - if (errorMessages.isPresent()) { + Optional> errorMessages = null; + if(executableInject.getExercise() != null) { + errorMessages = getErrorMessagesPreExecution(executableInject.getExercise().getId(), inject); + } + if (errorMessages != null && errorMessages.isPresent()) { InjectStatus status = new InjectStatus(); if (inject.getStatus().isEmpty()) { status.setInject(inject); diff --git a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx index 2593f0ccbf..accc9804fa 100644 --- a/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx +++ b/openbas-front/src/admin/components/common/injects/InjectChainsForm.tsx @@ -22,7 +22,7 @@ import { useFormatter } from '../../../../components/i18n'; import ClickableModeChip from '../../../../components/common/chips/ClickableModeChip'; import ClickableChip from '../../../../components/common/chips/ClickableChip'; import { capitalize } from '../../../../utils/String'; -import type { Inject, InjectDependency, InjectOutput } from '../../../../utils/api-types'; +import type { Inject, InjectDependency, InjectDependencyCondition, InjectOutput } from '../../../../utils/api-types'; import type { ConditionElement, ConditionType, Content, ConvertedContentType, Dependency, InjectOutputType } from '../../../../actions/injects/Inject'; import type { Element } from '../../../../components/common/chips/ClickableChip'; @@ -99,11 +99,11 @@ const InjectForm: React.FC = ({ values, form, injects }) => { * Transform an inject dependency into ConditionElement * @param injectDependsOn an array of injectDependency */ - const getConditionContentParent = (injectDependsOn: InjectDependency[]) => { + const getConditionContentParent = (injectDependsOn: (InjectDependency | undefined)[]) => { const conditions: ConditionType[] = []; if (injectDependsOn) { injectDependsOn.forEach((parent) => { - if (parent !== null) { + if (parent !== undefined) { conditions.push({ parentId: parent.dependency_relationship?.inject_parent_id, childrenId: parent.dependency_relationship?.inject_children_id, @@ -189,24 +189,6 @@ const InjectForm: React.FC = ({ values, form, injects }) => { (dependsOn) => dependsOn.dependency_relationship?.inject_children_id !== values.inject_id, ); } - - const baseInjectDependency: InjectDependency = { - dependency_relationship: { - inject_parent_id: newInject?.inject_id, - inject_children_id: values.inject_id, - }, - dependency_condition: { - conditions: [ - { - key: 'Execution', - operator: 'eq', - value: true, - }, - ], - mode: 'and', - }, - }; - newInject!.inject_depends_on = [baseInjectDependency]; return { inject: newInject!, index: element.index, @@ -217,13 +199,30 @@ const InjectForm: React.FC = ({ values, form, injects }) => { setParents(newParents); + const baseInjectDependency: InjectDependency = { + dependency_relationship: { + inject_parent_id: newInject?.inject_id, + inject_children_id: values.inject_id, + }, + dependency_condition: { + conditions: [ + { + key: 'Execution', + operator: 'eq', + value: true, + }, + ], + mode: 'and', + }, + }; + form.mutators.setValue( 'inject_depends_on', - injectDependencyFromDependency(newParents), + [baseInjectDependency], ); if (newInject!.inject_depends_on !== null) { - setParentConditions(getConditionContentParent(newInject!.inject_depends_on!)); + setParentConditions(getConditionContentParent(injectDependencyFromDependency(newParents))); } }; @@ -341,6 +340,26 @@ const InjectForm: React.FC = ({ values, form, injects }) => { /** * Returns an updated depends on from a ConditionType * @param conditions + * @param switchIds + */ + const updateDependsCondition = (conditions: ConditionType) => { + const result: InjectDependencyCondition = { + mode: conditions.mode === 'and' ? 'and' : 'or', + conditions: conditions.conditionElement?.map((value) => { + return { + value: value.value, + key: value.key, + operator: 'eq', + }; + }), + }; + return result; + }; + + /** + * Returns an updated depends on from a ConditionType + * @param conditions + * @param switchIds */ const updateDependsOn = (conditions: ConditionType) => { const result: InjectDependency = { @@ -348,16 +367,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { inject_parent_id: conditions.parentId, inject_children_id: conditions.childrenId, }, - dependency_condition: { - mode: conditions.mode === 'and' ? 'and' : 'or', - conditions: conditions.conditionElement?.map((value) => { - return { - value: value.value, - key: value.key, - operator: 'eq', - }; - }), - }, + dependency_condition: updateDependsCondition(conditions), }; return result; }; @@ -367,7 +377,7 @@ const InjectForm: React.FC = ({ values, form, injects }) => { * @param inject */ const getAvailableExpectations = (inject: InjectOutputType | undefined) => { - if (inject?.inject_content !== null && inject?.inject_content !== undefined) { + if (inject?.inject_content !== null && inject?.inject_content !== undefined && (inject.inject_content as Content).expectations !== undefined) { const expectations = (inject.inject_content as Content).expectations.map((expectation) => (expectation.expectation_type === 'MANUAL' ? expectation.expectation_name : capitalize(expectation.expectation_type))); return ['Execution', ...expectations]; } @@ -390,7 +400,6 @@ const InjectForm: React.FC = ({ values, form, injects }) => { const currentConditions = parentConditions.find((currentCondition) => parent.inject!.inject_id === currentCondition.parentId); if (parent.inject !== undefined && currentConditions !== undefined) { - const updatedParent = parents.find((currentParent) => currentParent.inject?.inject_id === parent.inject?.inject_id); let expectationString = 'Execution'; if (currentConditions?.conditionElement !== undefined) { expectationString = getAvailableExpectations(parent.inject) @@ -403,14 +412,30 @@ const InjectForm: React.FC = ({ values, form, injects }) => { index: currentConditions.conditionElement?.length, }); - if (updatedParent?.inject?.inject_depends_on !== undefined) { - updatedParent.inject.inject_depends_on = [updateDependsOn(currentConditions)]; - } - setParentConditions(parentConditions); + + const element = parentConditions.find((conditionElement) => conditionElement.childrenId === values.inject_id); + + const dep: InjectDependency = { + dependency_relationship: { + inject_parent_id: element?.parentId, + inject_children_id: element?.childrenId, + }, + dependency_condition: { + mode: element?.mode === '&&' ? 'and' : 'or', + conditions: element?.conditionElement ? element?.conditionElement.map((value) => { + return { + key: value.key, + value: value.value, + operator: 'eq', + }; + }) : [], + }, + }; + form.mutators.setValue( 'inject_depends_on', - injectDependencyFromDependency(parents), + [dep], ); } }; @@ -478,14 +503,27 @@ const InjectForm: React.FC = ({ values, form, injects }) => { }); setParentConditions(newParentConditions); - const updatedParent = parents.find((currentParent) => currentParent.inject?.inject_id === parent.inject?.inject_id); - const newCondition = newParentConditions.find((parentCondition) => parentCondition.parentId === parent.inject?.inject_id); - if (updatedParent?.inject?.inject_depends_on !== undefined && newCondition !== undefined) { - updatedParent.inject.inject_depends_on = [updateDependsOn(newCondition)]; - } + const element = newParentConditions?.find((conditionElement) => conditionElement.parentId === conditions.parentId); + const dep: InjectDependency = { + dependency_relationship: { + inject_parent_id: element?.parentId, + inject_children_id: element?.childrenId, + }, + dependency_condition: { + mode: element?.mode === '&&' ? 'and' : 'or', + conditions: element?.conditionElement ? element?.conditionElement.map((value) => { + return { + key: value.key, + value: value.value, + operator: 'eq', + }; + }) : [], + }, + }; + form.mutators.setValue( 'inject_depends_on', - injectDependencyFromDependency(parents), + [dep], ); }; @@ -549,14 +587,27 @@ const InjectForm: React.FC = ({ values, form, injects }) => { setParentConditions(newConditionElements); } - const newCurrentCondition = newConditionElements?.find((currentCondition) => currentCondition.parentId === condition.parentId); - const updatedParent = parents.find((currentParent) => currentParent.inject?.inject_id === newCurrentCondition?.parentId); - if (updatedParent?.inject?.inject_depends_on !== undefined && newCurrentCondition !== undefined) { - updatedParent.inject.inject_depends_on = [updateDependsOn(newCurrentCondition)]; - } + const element = newConditionElements?.find((conditionElement) => conditionElement.parentId === condition.parentId); + const dep: InjectDependency = { + dependency_relationship: { + inject_parent_id: element?.parentId, + inject_children_id: element?.childrenId, + }, + dependency_condition: { + mode: element?.mode === '&&' ? 'and' : 'or', + conditions: element?.conditionElement ? element?.conditionElement.map((value) => { + return { + key: value.key, + value: value.value, + operator: 'eq', + }; + }) : [], + }, + }; + form.mutators.setValue( 'inject_depends_on', - injectDependencyFromDependency(parents), + [dep], ); }; @@ -607,13 +658,27 @@ const InjectForm: React.FC = ({ values, form, injects }) => { }); setParentConditions(newConditionElements); - const updatedParent = parents.find((currentParent) => currentParent.inject?.inject_id === conditions?.parentId); - if (updatedParent?.inject?.inject_depends_on !== undefined && conditions !== undefined) { - updatedParent.inject.inject_depends_on = [updateDependsOn(conditions)]; - } + const element = newConditionElements.find((conditionElement) => conditionElement.parentId === conditions.parentId); + const dep: InjectDependency = { + dependency_relationship: { + inject_parent_id: element?.parentId, + inject_children_id: element?.childrenId, + }, + dependency_condition: { + mode: element?.mode === '&&' ? 'and' : 'or', + conditions: element?.conditionElement ? element?.conditionElement.map((value) => { + return { + key: value.key, + value: value.value, + operator: 'eq', + }; + }) : [], + }, + }; + form.mutators.setValue( 'inject_depends_on', - injectDependencyFromDependency(parents), + [dep], ); }; @@ -636,7 +701,8 @@ const InjectForm: React.FC = ({ values, form, injects }) => { const updatedChildren = childrens.find((currentChildren) => currentChildren.inject?.inject_id === conditions.childrenId); if (updatedChildren?.inject?.inject_depends_on !== undefined && conditions !== undefined) { - updatedChildren.inject.inject_depends_on = [updateDependsOn(conditions)]; + const newCondition = newConditionElements.find((currentCondition) => currentCondition.childrenId === conditions.childrenId); + if (newCondition !== undefined) updatedChildren.inject.inject_depends_on = [updateDependsOn(newCondition)]; } form.mutators.setValue( 'inject_depends_to', diff --git a/openbas-front/src/components/ChainedTimeline.tsx b/openbas-front/src/components/ChainedTimeline.tsx index 694bf94ffc..5236a084f6 100644 --- a/openbas-front/src/components/ChainedTimeline.tsx +++ b/openbas-front/src/components/ChainedTimeline.tsx @@ -37,6 +37,7 @@ import { useFormatter } from './i18n'; import type { AssetGroupsHelper } from '../actions/asset_groups/assetgroup-helper'; import type { EndpointHelper } from '../actions/assets/asset-helper'; import type { Inject, InjectDependency } from '../utils/api-types'; +import ChainingUtils from './common/chaining/ChainingUtils'; const useStyles = makeStyles(() => ({ container: { @@ -121,6 +122,8 @@ const ChainedTimelineFlow: FunctionComponent = ({ ]; const gapSize = 125; const newNodeSize = 50; + const nodeHeightClearance = 220; + const nodeWidthClearance = 350; let startDate: string | undefined; @@ -147,32 +150,102 @@ const ChainedTimelineFlow: FunctionComponent = ({ return Math.round(((position.x) / (gapSize / minutesPerGapAllowed[minutesPerGapIndex])) * 60); }; + /** + * Move item from an index to another one + * @param array the array to update + * @param to the target index + * @param from the origin index + */ + const moveItem = (array: NodeInject[], to: number, from: number) => { + const item = array[from]; + array.splice(from, 1); + array.splice(to, 0, item); + return array; + }; + + /** + * Calculate a bounding box for an index + * @param currentNode the node to calculate the bounding box for + * @param nodesAvailable the nodes + */ + const calculateBoundingBox = (currentNode: NodeInject, nodesAvailable: NodeInject[]) => { + if (currentNode.data.inject?.inject_depends_on) { + const nodesId = currentNode.data.inject?.inject_depends_on.map((value) => value.dependency_relationship?.inject_parent_id); + const dependencies = nodesAvailable.filter((dependencyNode) => nodesId.includes(dependencyNode.id)); + const minX = Math.min(currentNode.position.x, ...dependencies.map((value) => value.data.boundingBox!.topLeft.x)); + const minY = Math.min(currentNode.position.y, ...dependencies.map((value) => value.data.boundingBox!.topLeft.y)); + const maxX = Math.max(currentNode.position.x + nodeWidthClearance, ...dependencies.map((value) => value.data.boundingBox!.bottomRight.x)); + const maxY = Math.max(currentNode.position.y + nodeHeightClearance, ...dependencies.map((value) => value.data.boundingBox!.bottomRight.y)); + return { + topLeft: { x: minX, y: minY }, + bottomRight: { x: maxX, y: maxY }, + }; + } + return { + topLeft: currentNode.position, + bottomRight: { x: currentNode.position.x + nodeWidthClearance, y: currentNode.position.y + nodeHeightClearance }, + }; + }; + /** * Calculate injects position when dragging stopped * @param nodeInjects the list of injects */ const calculateInjectPosition = (nodeInjects: NodeInject[]) => { - nodeInjects.forEach((nodeInject, index) => { - let row = 0; - let rowFound = true; + let reorganizedInjects = nodeInjects; + + nodeInjects.forEach((node, i) => { + let childrens = reorganizedInjects.slice(i).filter((nextNode) => nextNode.id !== node.id + && nextNode.data.inject?.inject_depends_on !== undefined + && nextNode.data.inject?.inject_depends_on !== null + && nextNode.data.inject!.inject_depends_on + .find((dependsOn) => dependsOn.dependency_relationship?.inject_parent_id === node.id) !== undefined); + + childrens = childrens.sort((a, b) => a.data.inject!.inject_depends_duration - b.data.inject!.inject_depends_duration); + + childrens.forEach((children, j) => { + reorganizedInjects = moveItem(reorganizedInjects, i + j + 1, reorganizedInjects.indexOf(children, i)); + }); + }); + + reorganizedInjects.forEach((nodeInject, index) => { const nodeInjectPosition = nodeInject.position; const nodeInjectData = nodeInject.data; - do { - const previousNodes = nodeInjects.slice(0, index) - .filter((previousNode) => nodeInject.position.x >= previousNode.position.x && nodeInject.position.x < previousNode.position.x + 250); - - for (let i = 0; i < previousNodes.length; i += 1) { - const previousNode = previousNodes[i]; - if (previousNode.position.y + 150 > row * 150 && previousNode.position.y <= row * 150) { - row += 1; - rowFound = false; - } else { - nodeInjectPosition.y = 150 * row; - nodeInjectData.fixedY = nodeInject.position.y; - rowFound = true; - } + + const previousNodes = reorganizedInjects.slice(0, index) + .filter((previousNode) => previousNode.data.boundingBox !== undefined + && nodeInjectData.boundingBox !== undefined + && nodeInjectData.boundingBox?.topLeft.x >= previousNode.data.boundingBox.topLeft.x + && nodeInjectData.boundingBox?.topLeft.x < previousNode.data.boundingBox.bottomRight.x); + + const arrayOfY = previousNodes + .map((previousNode) => (previousNode.data.boundingBox?.bottomRight.y ? previousNode.data.boundingBox?.bottomRight.y : 0)); + const maxY = Math.max(0, ...arrayOfY); + + nodeInjectPosition.y = 0; + let rowFound = false; + for (let row = 1; row <= (maxY / nodeHeightClearance) + 1; row += 1) { + if (!arrayOfY.includes(row * nodeHeightClearance)) { + nodeInjectPosition.y = (row - 1) * nodeHeightClearance; + rowFound = true; + break; } - } while (!rowFound); + } + + if (!rowFound) { + nodeInjectPosition.y = previousNodes.length === 0 ? 0 : maxY; + } + if (nodeInject.data.inject?.inject_depends_on) { + const nodesId = nodeInject.data.inject?.inject_depends_on.map((value) => value.dependency_relationship?.inject_parent_id); + const dependencies = reorganizedInjects.filter((dependencyNode) => nodesId.includes(dependencyNode.id)); + const minY = dependencies.length > 0 ? Math.min(...dependencies.map((value) => value.data.boundingBox!.topLeft.y)) : 0; + + nodeInjectPosition.y = nodeInjectPosition.y < minY ? minY : nodeInjectPosition.y; + } + + nodeInjectData.fixedY = nodeInjectPosition.y; + nodeInjectData.boundingBox = calculateBoundingBox(nodeInject, reorganizedInjects); + reorganizedInjects[index] = nodeInject; }); }; @@ -189,9 +262,9 @@ const ChainedTimelineFlow: FunctionComponent = ({ targetHandle: `target-${inject.inject_depends_on[i].dependency_relationship?.inject_children_id}`, source: `${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}`, sourceHandle: `source-${inject.inject_depends_on[i].dependency_relationship?.inject_parent_id}`, - label: '', + label: ChainingUtils.fromInjectDependencyToLabel(inject.inject_depends_on[i]), labelShowBg: false, - labelStyle: { fill: theme.palette.text?.primary, fontSize: 9 }, + labelStyle: { fill: theme.palette.text?.primary, fontSize: 14 }, }); } } @@ -226,6 +299,10 @@ const ChainedTimelineFlow: FunctionComponent = ({ fixedY: 0, startDate, onSelectedInject, + boundingBox: { + topLeft: { x: (inject.inject_depends_duration / 60) * (gapSize / minutesPerGapAllowed[minutesPerGapIndex]), y: 0 }, + bottomRight: { x: (inject.inject_depends_duration / 60) * (gapSize / minutesPerGapAllowed[minutesPerGapIndex]) + nodeWidthClearance, y: nodeHeightClearance }, + }, targets: inject.inject_assets!.map((asset) => assets[asset]?.asset_name) .concat(inject.inject_asset_groups!.map((assetGroup) => assetGroups[assetGroup]?.asset_group_name)) .concat(inject.inject_teams!.map((team) => teams[team]?.team_name)), diff --git a/openbas-front/src/components/common/chaining/ChainingUtils.tsx b/openbas-front/src/components/common/chaining/ChainingUtils.tsx new file mode 100644 index 0000000000..6946df710e --- /dev/null +++ b/openbas-front/src/components/common/chaining/ChainingUtils.tsx @@ -0,0 +1,14 @@ +import type { InjectDependency } from '../../../utils/api-types'; + +const fromInjectDependencyToLabel = (dependency: InjectDependency) => { + let label = ''; + if (dependency.dependency_condition?.conditions !== undefined) { + label = dependency.dependency_condition.conditions + .map((value) => `${value.key} is ${value.value ? 'Success' : 'Failure'}`) + .join(dependency.dependency_condition.mode === 'and' ? ' AND ' : ' OR '); + } + + return label; +}; + +export default { fromInjectDependencyToLabel }; diff --git a/openbas-front/src/components/nodes/NodeInject.tsx b/openbas-front/src/components/nodes/NodeInject.tsx index 48c38a65e3..46c23add77 100644 --- a/openbas-front/src/components/nodes/NodeInject.tsx +++ b/openbas-front/src/components/nodes/NodeInject.tsx @@ -1,5 +1,5 @@ import React, { memo } from 'react'; -import { Handle, NodeProps, Position, Node, OnConnect } from '@xyflow/react'; +import { Handle, NodeProps, Position, Node, OnConnect, XYPosition } from '@xyflow/react'; import { makeStyles, useTheme } from '@mui/styles'; import { Tooltip } from '@mui/material'; import moment from 'moment'; @@ -89,6 +89,10 @@ export type NodeInject = Node<{ fixedY?: number, startDate?: string, targets: string[], + boundingBox?: { + topLeft: XYPosition, + bottomRight: XYPosition + }, exerciseOrScenarioId: string, onSelectedInject(inject?: InjectOutputType): void, onCreate: (result: { result: string, entities: { injects: Record } }) => void,