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 3af886fb0c..38278ae5df 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 @@ -21,6 +21,7 @@ public class InjectOutput { private String id; @JsonProperty("inject_title") + @NotBlank private String title; @JsonProperty("inject_enabled") diff --git a/openbas-api/src/test/java/io/openbas/rest/ScenarioInjectApiSearchTest.java b/openbas-api/src/test/java/io/openbas/rest/ScenarioInjectApiSearchTest.java new file mode 100644 index 0000000000..b9ff610782 --- /dev/null +++ b/openbas-api/src/test/java/io/openbas/rest/ScenarioInjectApiSearchTest.java @@ -0,0 +1,185 @@ +package io.openbas.rest; + +import io.openbas.IntegrationTest; +import io.openbas.database.model.Inject; +import io.openbas.database.model.InjectorContract; +import io.openbas.database.model.Scenario; +import io.openbas.database.repository.InjectRepository; +import io.openbas.database.repository.InjectorContractRepository; +import io.openbas.database.repository.ScenarioRepository; +import io.openbas.utils.fixtures.PaginationFixture; +import io.openbas.utils.mockUser.WithMockAdminUser; +import io.openbas.utils.pagination.SearchPaginationInput; +import io.openbas.utils.pagination.SortField; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.List; + +import static io.openbas.database.model.Filters.FilterOperator.contains; +import static io.openbas.injectors.email.EmailContract.EMAIL_DEFAULT; +import static io.openbas.rest.scenario.ScenarioApi.SCENARIO_URI; +import static io.openbas.utils.JsonUtils.asJsonString; +import static io.openbas.utils.fixtures.InjectFixture.getInjectForEmailContract; +import static io.openbas.utils.fixtures.ScenarioFixture.createDefaultCrisisScenario; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@TestInstance(PER_CLASS) +public class ScenarioInjectApiSearchTest extends IntegrationTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private InjectRepository injectRepository; + @Autowired + private InjectorContractRepository injectorContractRepository; + @Autowired + private ScenarioRepository scenarioRepository; + + private static final List INJECT_IDS = new ArrayList<>(); + private static String SCENARIO_ID; + private static String EMAIL_INJECTOR_CONTRACT_ID; + + @BeforeAll + void beforeAll() { + InjectorContract injectorContract = this.injectorContractRepository.findById(EMAIL_DEFAULT).orElseThrow(); + EMAIL_INJECTOR_CONTRACT_ID = injectorContract.getInjector().getId(); + + Scenario scenario = createDefaultCrisisScenario(); + Scenario scenarioSaved = this.scenarioRepository.save(scenario); + SCENARIO_ID = scenarioSaved.getId(); + + Inject injectDefaultEmail = getInjectForEmailContract(injectorContract); + injectDefaultEmail.setScenario(scenarioSaved); + injectDefaultEmail.setTitle("Inject default email"); + injectDefaultEmail.setDependsDuration(1L); + Inject injectDefaultEmailSaved = this.injectRepository.save(injectDefaultEmail); + INJECT_IDS.add(injectDefaultEmailSaved.getId()); + + Inject injectDefaultGlobal = getInjectForEmailContract(injectorContract); + injectDefaultGlobal.setScenario(scenarioSaved); + injectDefaultGlobal.setTitle("Inject global email"); + Inject injectDefaultGlobalSaved = this.injectRepository.save(injectDefaultGlobal); + INJECT_IDS.add(injectDefaultGlobalSaved.getId()); + } + + @AfterAll + void afterAll() { + this.injectRepository.deleteAllById(INJECT_IDS); + this.scenarioRepository.deleteById(SCENARIO_ID); + } + + @Nested + @WithMockAdminUser + @DisplayName("Retrieving injects") + class RetrievingInjects { + // -- PREPARE -- + + @Nested + @DisplayName("Searching page of injects") + class SearchingPageOfInjects { + + @Test + @DisplayName("Retrieving first page of injects by textsearch") + void given_working_search_input_should_return_a_page_of_injects() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().textSearch("default").build(); + + mvc.perform(post(SCENARIO_URI + "/" + SCENARIO_ID + "/injects/simple") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(1)); + } + + @Test + @DisplayName("Not retrieving first page of injects by textsearch") + void given_not_working_search_input_should_return_a_page_of_injects() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault().textSearch("wrong").build(); + + mvc.perform(post(SCENARIO_URI + "/" + SCENARIO_ID + "/injects/simple") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(0)); + } + } + + @Nested + @DisplayName("Sorting page of injects") + class SortingPageOfInjects { + + @Test + @DisplayName("Sorting page of injects by name") + void given_sorting_input_by_name_should_return_a_page_of_injects_sort_by_name() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault() + .sorts(List.of(SortField.builder().property("inject_title").build())) + .build(); + + mvc.perform(post(SCENARIO_URI + "/" + SCENARIO_ID + "/injects/simple") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.content.[0].inject_title").value("Inject default email")) + .andExpect(jsonPath("$.content.[1].inject_title").value("Inject global email")); + } + + @Test + @DisplayName("Sorting page of injects by updated at") + void given_sorting_input_by_updated_at_should_return_a_page_of_injects_sort_by_updated_at() + throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.getDefault() + .sorts(List.of(SortField.builder().property("inject_depends_duration").direction("asc").build())) + .build(); + + mvc.perform(post(SCENARIO_URI + "/" + SCENARIO_ID + "/injects/simple") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.content.[0].inject_title").value("Inject global email")) + .andExpect(jsonPath("$.content.[1].inject_title").value("Inject default email")); + } + } + + @Nested + @DisplayName("Filtering page of injects") + class FilteringPageOfInjects { + + @Test + @DisplayName("Filtering page of injects by name") + void given_filter_input_by_name_should_return_a_page_of_injects_filter_by_name() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.simpleFilter( + "inject_title", "email", contains + ); + + mvc.perform(post(SCENARIO_URI + "/" + SCENARIO_ID + "/injects/simple") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(2)); + } + + @Test + @DisplayName("Filtering page of injects by injector contract") + void given_filter_input_by_injector_contract_should_return_a_page_of_injects_filter_by_injector_contract() throws Exception { + SearchPaginationInput searchPaginationInput = PaginationFixture.simpleFilter( + "inject_injector_contract", EMAIL_INJECTOR_CONTRACT_ID, contains + ); + + mvc.perform(post(SCENARIO_URI + "/" + SCENARIO_ID + "/injects/simple") + .contentType(MediaType.APPLICATION_JSON) + .content(asJsonString(searchPaginationInput))) + .andExpect(status().is2xxSuccessful()) + .andExpect(jsonPath("$.numberOfElements").value(2)); + } + } + + } + +} diff --git a/openbas-framework/src/main/java/io/openbas/utils/schema/SchemaUtils.java b/openbas-framework/src/main/java/io/openbas/utils/schema/SchemaUtils.java index 251a4f6b1a..cb1311ab8f 100644 --- a/openbas-framework/src/main/java/io/openbas/utils/schema/SchemaUtils.java +++ b/openbas-framework/src/main/java/io/openbas/utils/schema/SchemaUtils.java @@ -13,7 +13,6 @@ import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -35,27 +34,6 @@ private SchemaUtils() { Email.class ); - public static final Class[] BASE_CLASSES = { - byte.class, - short.class, - int.class, - long.class, - float.class, - double.class, - char.class, - boolean.class, - Byte.class, - Short.class, - Integer.class, - Long.class, - Float.class, - Double.class, - Character.class, - Boolean.class, - String.class, - Instant.class, - }; - private static final ConcurrentHashMap, List> cacheMap = new ConcurrentHashMap<>(); // -- SCHEMA -- diff --git a/openbas-front/src/actions/injects/Inject.d.ts b/openbas-front/src/actions/injects/Inject.d.ts index 7335cfe6f0..9d70a9a315 100644 --- a/openbas-front/src/actions/injects/Inject.d.ts +++ b/openbas-front/src/actions/injects/Inject.d.ts @@ -1,13 +1,4 @@ -import type { Inject, InjectExpectation } from '../../utils/api-types'; - -export type InjectInput = { - inject_injector_contract: { id: string, type: string }; - inject_tags: string[]; - inject_depends_duration_days: number; - inject_depends_duration_hours: number; - inject_depends_duration_minutes: number; - inject_depends_duration_seconds: number; -}; +import type { Inject, InjectExpectation, InjectOutput } from '../../utils/api-types'; export type InjectStore = Omit & { inject_tags: string[] | undefined; @@ -17,11 +8,33 @@ export type InjectStore = Omit + config: { + expose: boolean + } + } } & Inject['inject_injector_contract'] inject_exercise?: string inject_scenario?: string }; +export type InjectorContractConvertedContent = { + label: Record + config: { + expose: boolean + } +}; + +export type InjectOutputType = InjectOutput & { + inject_injector_contract: { + // as we don't know the type of the content of a contract we need to put any here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + injector_contract_content_parsed: any + convertedContent: InjectorContractConvertedContent + } & Inject['inject_injector_contract'] +}; + export type InjectExpectationStore = Omit & { inject_expectation_team: string | undefined; inject_expectation_inject: string | undefined; diff --git a/openbas-front/src/actions/injects/inject-action.ts b/openbas-front/src/actions/injects/inject-action.ts index 6471ecedbe..4a28178539 100644 --- a/openbas-front/src/actions/injects/inject-action.ts +++ b/openbas-front/src/actions/injects/inject-action.ts @@ -1,6 +1,6 @@ import { Dispatch } from 'redux'; import { getReferential, simpleCall, simplePostCall } from '../../utils/Action'; -import type { Exercise, Scenario } from '../../utils/api-types'; +import type { Exercise, Scenario, SearchPaginationInput } from '../../utils/api-types'; import * as schema from '../Schema'; export const testInject = (injectId: string) => { @@ -21,9 +21,19 @@ export const fetchExerciseInjectsSimple = (exerciseId: Exercise['exercise_id']) return getReferential(schema.arrayOfInjects, uri)(dispatch); }; +export const searchExerciseInjectsSimple = (exerciseId: Exercise['exercise_id'], input: SearchPaginationInput) => { + const uri = `/api/exercises/${exerciseId}/injects/simple`; + return simplePostCall(uri, input); +}; + // -- SCENARIOS -- export const fetchScenarioInjectsSimple = (scenarioId: Scenario['scenario_id']) => (dispatch: Dispatch) => { const uri = `/api/scenarios/${scenarioId}/injects/simple`; return getReferential(schema.arrayOfInjects, uri)(dispatch); }; + +export const searchScenarioInjectsSimple = (scenarioId: Scenario['scenario_id'], input: SearchPaginationInput) => { + const uri = `/api/scenarios/${scenarioId}/injects/simple`; + return simplePostCall(uri, input); +}; diff --git a/openbas-front/src/actions/scenarios/scenario-actions.ts b/openbas-front/src/actions/scenarios/scenario-actions.ts index 81d9264b6c..282678c750 100644 --- a/openbas-front/src/actions/scenarios/scenario-actions.ts +++ b/openbas-front/src/actions/scenarios/scenario-actions.ts @@ -14,7 +14,6 @@ import type { ScenarioInput, ScenarioRecurrenceInput, ScenarioTeamPlayersEnableInput, - ScenarioUpdateTagsInput, ScenarioUpdateTeamsInput, SearchPaginationInput, Team, @@ -78,13 +77,6 @@ export const duplicateScenario = (scenarioId: string) => (dispatch: Dispatch) => return postReferential(scenario, uri, null)(dispatch); }; -// -- TAGS -- - -export const updateScenarioTags = (scenarioId: Scenario['scenario_id'], data: ScenarioUpdateTagsInput) => { - const uri = `${SCENARIO_URI}/${scenarioId}/tags`; - return putReferential(scenario, uri, data); -}; - // -- TEAMS -- export const fetchScenarioTeams = (scenarioId: Scenario['scenario_id']) => (dispatch: Dispatch) => { diff --git a/openbas-front/src/admin/components/common/Context.ts b/openbas-front/src/admin/components/common/Context.ts index bdd38d8461..4723cc389a 100644 --- a/openbas-front/src/admin/components/common/Context.ts +++ b/openbas-front/src/admin/components/common/Context.ts @@ -20,13 +20,15 @@ import type { LessonsSendInput, Objective, ObjectiveInput, + SearchPaginationInput, Team, TeamCreateInput, Variable, VariableInput, } from '../../../utils/api-types'; import type { UserStore } from '../teams/players/Player'; -import type { InjectStore } from '../../../actions/injects/Inject'; +import type { InjectOutputType, InjectStore } from '../../../actions/injects/Inject'; +import { Page } from '../../../components/common/queryable/Page'; export type PermissionsContextType = { permissions: { readOnly: boolean, canWrite: boolean, isRunning: boolean } @@ -69,8 +71,9 @@ export type TeamContextType = { }; export type InjectContextType = { - onAddInject: (inject: Inject) => Promise<{ result: string }>, - onUpdateInject: (injectId: Inject['inject_id'], inject: Inject) => Promise<{ result: string }>, + searchInjects: (input: SearchPaginationInput) => Promise<{ data: Page }>, + onAddInject: (inject: Inject) => Promise<{ result: string, entities: { injects: Record } }>, + onUpdateInject: (injectId: Inject['inject_id'], inject: Inject) => Promise<{ result: string, entities: { injects: Record } }>, onUpdateInjectTrigger?: (injectId: Inject['inject_id']) => void, onUpdateInjectActivation: (injectId: Inject['inject_id'], injectEnabled: { inject_enabled: boolean }) => Promise<{ result: string, @@ -156,11 +159,15 @@ export const TeamContext = createContext({ }, }); export const InjectContext = createContext({ - onAddInject(_inject: Inject): Promise<{ result: string }> { - return Promise.resolve({ result: '' }); + searchInjects(_: SearchPaginationInput): Promise<{ data: Page }> { + return new Promise<{ data: Page }>(() => { + }); }, - onUpdateInject(_injectId: Inject['inject_id'], _inject: Inject): Promise<{ result: string }> { - return Promise.resolve({ result: '' }); + onAddInject(_inject: Inject): Promise<{ result: string, entities: { injects: Record } }> { + return Promise.resolve({ result: '', entities: { injects: {} } }); + }, + onUpdateInject(_injectId: Inject['inject_id'], _inject: Inject): Promise<{ result: string, entities: { injects: Record } }> { + return Promise.resolve({ result: '', entities: { injects: {} } }); }, onUpdateInjectTrigger(_injectId: Inject['inject_id']): void { }, diff --git a/openbas-front/src/admin/components/common/injects/Injects.js b/openbas-front/src/admin/components/common/injects/Injects.js deleted file mode 100644 index 7c6e63c0ab..0000000000 --- a/openbas-front/src/admin/components/common/injects/Injects.js +++ /dev/null @@ -1,633 +0,0 @@ -import React, { useContext, useState } from 'react'; -import { makeStyles } from '@mui/styles'; -import { - Checkbox, - Chip, - IconButton, - List, - ListItem, - ListItemIcon, - ListItemSecondaryAction, - ListItemText, - Menu, - MenuItem, - ToggleButton, - ToggleButtonGroup, - Tooltip, -} from '@mui/material'; -import { BarChartOutlined, MoreVert, ReorderOutlined } from '@mui/icons-material'; -import { splitDuration } from '../../../../utils/Time'; -import ItemTags from '../../../../components/ItemTags'; -import SearchFilter from '../../../../components/SearchFilter'; -import TagsFilter from '../filters/TagsFilter'; -import InjectIcon from './InjectIcon'; -import InjectPopover from './InjectPopover'; -import InjectorContract from './InjectorContract'; -import useSearchAnFilter from '../../../../utils/SortingFiltering'; -import { useFormatter } from '../../../../components/i18n'; -import { useHelper } from '../../../../store'; -import ItemBoolean from '../../../../components/ItemBoolean'; -import { exportData } from '../../../../utils/Environment'; -import Loader from '../../../../components/Loader'; -import { InjectContext, PermissionsContext } from '../Context'; -import CreateInject from './CreateInject'; -import UpdateInject from './UpdateInject'; -import PlatformIcon from '../../../../components/PlatformIcon'; -import ChainedTimeline from '../../../../components/ChainedTimeline'; -import { isNotEmptyField } from '../../../../utils/utils'; -import ImportUploaderInjectFromXls from './ImportUploaderInjectFromXls'; -import useExportToXLS from '../../../../utils/hooks/useExportToXLS'; -import ButtonCreate from '../../../../components/common/ButtonCreate'; - -const useStyles = makeStyles(() => ({ - container: { - margin: '-12px 0 50px 0', - }, - disabled: { - opacity: 0.38, - pointerEvents: 'none', - }, - itemHead: { - paddingLeft: 10, - textTransform: 'uppercase', - cursor: 'pointer', - }, - item: { - paddingLeft: 10, - height: 50, - }, - bodyItem: { - fontSize: 13, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - paddingRight: 10, - }, - duration: { - fontSize: 12, - lineHeight: '12px', - height: 20, - float: 'left', - marginRight: 7, - borderRadius: 4, - width: 180, - backgroundColor: 'rgba(0, 177, 255, 0.08)', - color: '#00b1ff', - border: '1px solid #00b1ff', - }, -})); - -const headerStyles = { - iconSort: { - position: 'absolute', - margin: '0 0 0 5px', - padding: 0, - top: 9, - }, - inject_type: { - float: 'left', - width: '15%', - fontSize: 12, - fontWeight: '700', - }, - inject_title: { - float: 'left', - width: '25%', - fontSize: 12, - fontWeight: '700', - }, - inject_depends_duration: { - float: 'left', - width: '18%', - fontSize: 12, - fontWeight: '700', - }, - inject_platforms: { - float: 'left', - width: '10%', - fontSize: 12, - fontWeight: '700', - }, - inject_enabled: { - float: 'left', - width: '12%', - fontSize: 12, - fontWeight: '700', - }, - inject_tags: { - float: 'left', - width: '20%', - fontSize: 12, - fontWeight: '700', - }, -}; - -const inlineStyles = { - inject_type: { - float: 'left', - width: '15%', - height: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - inject_title: { - float: 'left', - width: '25%', - height: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - inject_depends_duration: { - float: 'left', - width: '18%', - height: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - inject_platforms: { - float: 'left', - width: '10%', - height: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - display: 'flex', - alignItems: 'center', - }, - inject_enabled: { - float: 'left', - width: '12%', - height: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - inject_tags: { - float: 'left', - width: '20%', - height: 20, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, -}; - -const Injects = (props) => { - const { - exerciseOrScenarioId, - injects, - teams, - articles, - variables, - uriVariable, - allUsersNumber, - usersNumber, - teamsUsers, - setViewMode, - onToggleEntity, - selectedElements, - deSelectedElements, - selectAll, - onToggleShiftEntity, - handleToggleSelectAll, - onConnectInjects, - } = props; - // Standard hooks - const classes = useStyles(); - const { t, tPick } = useFormatter(); - const [selectedInjectId, setSelectedInjectId] = useState(null); - const [openCreateDrawer, setOpenCreateDrawer] = useState(false); - const [presetCreationValues, setPresetCreationValues] = useState(); - const [showTimeline, setShowTimeline] = useState( - () => { - const storedValue = localStorage.getItem(`${exerciseOrScenarioId}_show_injects_timeline`); - return storedValue === null ? true : storedValue === 'true'; - }, - ); - const { permissions } = useContext(PermissionsContext); - const injectContext = useContext(InjectContext); - - // Filter and sort hook - const searchColumns = ['title', 'description', 'content']; - const filtering = useSearchAnFilter( - 'inject', - 'depends_duration', - searchColumns, - ); - // Fetching data - const { - tagsMap, - } = useHelper((helper) => { - return { - tagsMap: helper.getTagsMap(), - }; - }); - const onCreateInject = async (data) => { - await injectContext.onAddInject(data); - }; - const onUpdateInject = async (data) => { - await injectContext.onUpdateInject(selectedInjectId, data); - }; - - const sortedInjects = filtering.filterAndSort(injects); - - const isAtLeastOneValidInject = sortedInjects.some((inject) => inject.inject_injector_contract?.injector_contract_content_parsed !== null); - - // Menu - const [anchorEl, setAnchorEl] = useState(null); - - const exportInjects = exportData( - 'inject', - [ - 'inject_type', - 'inject_title', - 'inject_description', - 'inject_depends_duration', - 'inject_enabled', - 'inject_tags', - 'inject_content', - ], - sortedInjects, - tagsMap, - ); - - const exportInjectsToXLS = useExportToXLS({ data: exportInjects, fileName: `${t('Injects')}` }); - - const handleShowTimeline = () => { - setShowTimeline(!showTimeline); - localStorage.setItem(`${exerciseOrScenarioId}_show_injects_timeline`, !showTimeline); - setAnchorEl(null); - }; - - // Rendering - if (injects) { - return ( -
-
-
- -
-
- -
-
- {sortedInjects.length > 0 && ( -
- { - ev.stopPropagation(); - setAnchorEl(ev.currentTarget); - }} - > - - - setAnchorEl(null)} - > - - {`${t('Export injects')}`} - - {isAtLeastOneValidInject && ( - {showTimeline ? t('Hide timeline') : t('Show timeline')} - )} - -
- )} - - {injectContext.onImportInjectFromXls - && } - {setViewMode - && - - - - - } - {setViewMode - && - setViewMode('distribution')} - aria-label="Distribution view mode" - > - - - - } - -
-
-
- {showTimeline && isAtLeastOneValidInject && ( -
-
- { - setOpenCreateDrawer(true); - setPresetCreationValues(data); - }} - onSelectedInject={(inject) => { - const injectContract = inject.inject_injector_contract.convertedContent; - const isContractExposed = injectContract?.config.expose; - if (injectContract && isContractExposed) { - setSelectedInjectId(inject.inject_id); - } - }} - /> -
-
-
- )} - - - - - - - -   - - - - {filtering.buildHeader( - 'inject_type', - 'Type', - true, - headerStyles, - )} - {filtering.buildHeader( - 'inject_title', - 'Title', - true, - headerStyles, - )} - {filtering.buildHeader( - 'inject_depends_duration', - 'Trigger', - true, - headerStyles, - )} - {filtering.buildHeader( - 'inject_platforms', - 'Platform(s)', - true, - headerStyles, - )} - {filtering.buildHeader( - 'inject_enabled', - 'Status', - true, - headerStyles, - )} - {filtering.buildHeader( - 'inject_tags', - 'Tags', - true, - headerStyles, - )} - - } - /> -   - - {sortedInjects.map((inject, index) => { - const injectContract = inject.inject_injector_contract.convertedContent; - const injectorContractName = tPick(injectContract?.label); - const duration = splitDuration( - inject.inject_depends_duration || 0, - ); - const isContractExposed = injectContract?.config.expose; - let injectStatus = inject.inject_enabled - ? t('Enabled') - : t('Disabled'); - if (!inject.inject_ready) { - injectStatus = t('Missing content'); - } - return ( - { - if (injectContract && isContractExposed) { - setSelectedInjectId(inject.inject_id); - } - }} - > - (event.shiftKey - ? onToggleShiftEntity(index, inject, event) - : onToggleEntity(inject, event)) - } - > - - - - - - -
- {injectContract ? ( - - ) : - } -
-
- {inject.inject_title} -
-
- -
-
- {inject.inject_injector_contract?.injector_contract_platforms?.map( - (platform) => , - )} -
-
- -
-
- -
-
- } - /> - - - - - ); - })} - - {permissions.canWrite && ( - <> - {selectedInjectId !== null - && setSelectedInjectId(null)} - onUpdateInject={onUpdateInject} - injectId={selectedInjectId} - teamsFromExerciseOrScenario={teams} - articlesFromExerciseOrScenario={articles} - variablesFromExerciseOrScenario={variables} - uriVariable={uriVariable} - allUsersNumber={allUsersNumber} - usersNumber={usersNumber} - teamsUsers={teamsUsers} - /> - } - { - setOpenCreateDrawer(true); - setPresetCreationValues(undefined); - }} - /> - setOpenCreateDrawer(false)} - onCreateInject={onCreateInject} - presetValues={presetCreationValues} - teamsFromExerciseOrScenario={teams} - articlesFromExerciseOrScenario={articles} - variablesFromExerciseOrScenario={variables} - uriVariable={uriVariable} - allUsersNumber={allUsersNumber} - usersNumber={usersNumber} - teamsUsers={teamsUsers} - /> - - )} -
- ); - } - return ( -
- -
- ); -}; - -export default Injects; diff --git a/openbas-front/src/admin/components/common/injects/Injects.tsx b/openbas-front/src/admin/components/common/injects/Injects.tsx new file mode 100644 index 0000000000..df97922fdf --- /dev/null +++ b/openbas-front/src/admin/components/common/injects/Injects.tsx @@ -0,0 +1,473 @@ +import React, { CSSProperties, FunctionComponent, useContext, useMemo, useState } from 'react'; +import { Checkbox, Chip, List, ListItem, ListItemIcon, ListItemSecondaryAction, ListItemText } from '@mui/material'; +import { makeStyles } from '@mui/styles'; +import { Connection } from '@xyflow/react'; +import { splitDuration } from '../../../../utils/Time'; +import ItemTags from '../../../../components/ItemTags'; +import InjectIcon from './InjectIcon'; +import InjectPopover from './InjectPopover'; +import InjectorContract from './InjectorContract'; +import ItemBoolean from '../../../../components/ItemBoolean'; +import PlatformIcon from '../../../../components/PlatformIcon'; +import { isNotEmptyField } from '../../../../utils/utils'; +import { useFormatter } from '../../../../components/i18n'; +import type { FilterGroup, Inject, Variable } from '../../../../utils/api-types'; +import type { InjectorContractConvertedContent, InjectOutputType, InjectStore } from '../../../../actions/injects/Inject'; +import { InjectContext, PermissionsContext } from '../Context'; +import PaginationComponentV2 from '../../../../components/common/queryable/pagination/PaginationComponentV2'; +import useQueryable from '../../../../components/common/queryable/useQueryable'; +import { buildSearchPagination } from '../../../../components/common/queryable/QueryableUtils'; +import SortHeadersComponentV2 from '../../../../components/common/queryable/sort/SortHeadersComponentV2'; +import InjectsListButtons from './InjectsListButtons'; +import ChainedTimeline from '../../../../components/ChainedTimeline'; +import { initSorting } from '../../../../components/common/queryable/Page'; +import UpdateInject from './UpdateInject'; +import ButtonCreate from '../../../../components/common/ButtonCreate'; +import CreateInject from './CreateInject'; +import type { TeamStore } from '../../../../actions/teams/Team'; +import type { ArticleStore } from '../../../../actions/channels/Article'; +import { buildEmptyFilter } from '../../../../components/common/queryable/filter/FilterUtils'; + +const useStyles = makeStyles(() => ({ + disabled: { + opacity: 0.38, + pointerEvents: 'none', + }, + duration: { + fontSize: 12, + lineHeight: '12px', + height: 20, + float: 'left', + marginRight: 7, + borderRadius: 4, + width: 180, + backgroundColor: 'rgba(0, 177, 255, 0.08)', + color: '#00b1ff', + border: '1px solid #00b1ff', + }, + + itemHead: { + textTransform: 'uppercase', + }, + item: { + height: 50, + }, + bodyItems: { + display: 'flex', + }, + bodyItem: { + height: 20, + fontSize: 13, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + paddingRight: 10, + }, +})); + +const inlineStyles: Record = { + inject_type: { + width: '15%', + }, + inject_title: { + width: '25%', + }, + inject_depends_duration: { + width: '18%', + }, + inject_platforms: { + width: '10%', + }, + inject_enabled: { + width: '12%', + }, + inject_tags: { + width: '20%', + }, +}; + +interface Props { + selectAll: boolean + handleToggleSelectAll: () => void + onToggleEntity: (entity: { inject_id: string }, _?: React.SyntheticEvent, forceRemove?: { inject_id: string }[]) => void, + onToggleShiftEntity: (currentIndex: number, currentEntity: { inject_id: string }, event: React.SyntheticEvent | null) => void, + selectedElements: Record, + deSelectedElements: Record, + + exerciseOrScenarioId: string + + setViewMode?: (mode: string) => void + onConnectInjects: (connection: Connection) => void + + teams: TeamStore[] + articles: ArticleStore[] + variables: Variable[] + uriVariable: string + allUsersNumber?: number + usersNumber?: number + teamsUsers: never +} + +const Injects: FunctionComponent = ({ + selectAll, + handleToggleSelectAll, + onToggleEntity, + onToggleShiftEntity, + selectedElements, + deSelectedElements, + exerciseOrScenarioId, + setViewMode, + onConnectInjects, + teams, + articles, + variables, + uriVariable, + allUsersNumber, + usersNumber, + teamsUsers, +}) => { + // Standard hooks + const classes = useStyles(); + const { t, tPick } = useFormatter(); + const injectContext = useContext(InjectContext); + const { permissions } = useContext(PermissionsContext); + + // Headers + const headers = useMemo(() => [ + { + field: 'inject_type', + label: 'Type', + isSortable: false, + value: (_: InjectOutputType, injectContract: InjectorContractConvertedContent) => { + const injectorContractName = tPick(injectContract?.label); + return injectContract ? ( + + ) : ; + } + , + }, + { + field: 'inject_title', + label: 'Title', + isSortable: true, + value: (inject: InjectOutputType, _: InjectorContractConvertedContent) => inject.inject_title, + }, + { + field: 'inject_depends_duration', + label: 'Trigger', + isSortable: true, + value: (inject: InjectOutputType, _: InjectorContractConvertedContent) => { + const duration = splitDuration( + inject.inject_depends_duration || 0, + ); + return ; + }, + }, + { + field: 'inject_platforms', + label: 'Platform(s)', + isSortable: false, + value: (inject: InjectOutputType, _: InjectorContractConvertedContent) => inject.inject_injector_contract?.injector_contract_platforms?.map( + (platform) => , + ), + }, + { + field: 'inject_enabled', + label: 'Status', + isSortable: false, + value: (inject: InjectOutputType, _: InjectorContractConvertedContent) => { + let injectStatus = inject.inject_enabled + ? t('Enabled') + : t('Disabled'); + if (!inject.inject_ready) { + injectStatus = t('Missing content'); + } + return ; + }, + }, + { + field: 'inject_tags', + label: 'Tags', + isSortable: false, + value: (inject: InjectOutputType, _: InjectorContractConvertedContent) => , + }, + ], []); + + // Injects + const [injects, setInjects] = useState([]); + const [selectedInjectId, setSelectedInjectId] = useState(null); + + const onCreateInject = async (data: Inject) => { + await injectContext.onAddInject(data).then((result: { result: string, entities: { injects: Record } }) => { + if (result.entities) { + const created = result.entities.injects[result.result]; + setInjects([created as InjectOutputType, ...injects]); + } + }); + }; + const onUpdateInject = async (data: Inject) => { + if (selectedInjectId) { + await injectContext.onUpdateInject(selectedInjectId, data).then((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)))); + } + }); + } + }; + + const [openCreateDrawer, setOpenCreateDrawer] = useState(false); + const [presetCreationValues, setPresetCreationValues] = useState<{ + inject_depends_duration_days?: number, + inject_depends_duration_hours?: number, + inject_depends_duration_minutes?: number, + }>(); + + const openCreateInjectDrawer = (data: { + inject_depends_duration_days?: number, + inject_depends_duration_hours?: number, + inject_depends_duration_minutes?: number, + }) => { + setOpenCreateDrawer(true); + setPresetCreationValues(data); + }; + + // Timeline + const [showTimeline, setShowTimeline] = useState( + () => { + const storedValue = localStorage.getItem(`${exerciseOrScenarioId}_show_injects_timeline`); + return storedValue === null ? true : storedValue === 'true'; + }, + ); + const handleShowTimeline = () => { + setShowTimeline(!showTimeline); + localStorage.setItem(`${exerciseOrScenarioId}_show_injects_timeline`, String(!showTimeline)); + }; + + // Filters + const availableFilterNames = [ + 'inject_platforms', + 'inject_kill_chain_phases', + 'inject_injector_contract', + 'inject_type', + 'inject_title', + ]; + + const quickFilter: FilterGroup = { + mode: 'and', + filters: [ + buildEmptyFilter('inject_platforms', 'contains'), + buildEmptyFilter('inject_kill_chain_phases', 'contains'), + buildEmptyFilter('inject_injector_contract', 'contains'), + ], + }; + + const { queryableHelpers, searchPaginationInput } = useQueryable(`${exerciseOrScenarioId}-injects`, buildSearchPagination({ + sorts: initSorting('inject_depends_duration', 'ASC'), + filterGroup: quickFilter, + size: 100, + })); + + return ( + <> + injectContext.searchInjects(input)} + searchPaginationInput={searchPaginationInput} + setContent={setInjects} + entityPrefix="inject" + availableFilterNames={availableFilterNames} + queryableHelpers={queryableHelpers} + disablePagination + topBarButtons={ + + } + /> + {showTimeline && ( +
+
+ { + const injectContract = inject?.inject_injector_contract.convertedContent; + const isContractExposed = injectContract?.config.expose; + if (injectContract && isContractExposed) { + setSelectedInjectId(inject?.inject_id); + } + }} + /> +
+
+
+ )} + + + + + + + + } + /> + + + {injects.map((inject: InjectOutputType, index) => { + const injectContract = inject.inject_injector_contract?.convertedContent; + const isContractExposed = injectContract?.config.expose; + return ( + { + if (injectContract && isContractExposed) { + setSelectedInjectId(inject.inject_id); + } + }} + > + (event.shiftKey + ? onToggleShiftEntity(index, inject, event) + : onToggleEntity(inject, event)) + } + > + + + + + + +
+ {headers.map((header) => ( +
+ {header.value(inject, injectContract)} +
+ ))} +
+
+ } + /> + + + + + ); + })} + + {permissions.canWrite && ( + <> + {selectedInjectId !== null + && setSelectedInjectId(null)} + onUpdateInject={onUpdateInject} + injectId={selectedInjectId} + teamsFromExerciseOrScenario={teams} + // @ts-expect-error typing + articlesFromExerciseOrScenario={articles} + variablesFromExerciseOrScenario={variables} + uriVariable={uriVariable} + allUsersNumber={allUsersNumber} + usersNumber={usersNumber} + teamsUsers={teamsUsers} + /> + } + { + setOpenCreateDrawer(true); + setPresetCreationValues(undefined); + }} + /> + setOpenCreateDrawer(false)} + onCreateInject={onCreateInject} + presetValues={presetCreationValues} + // @ts-expect-error typing + teamsFromExerciseOrScenario={teams} + articlesFromExerciseOrScenario={articles} + variablesFromExerciseOrScenario={variables} + uriVariable={uriVariable} + allUsersNumber={allUsersNumber} + usersNumber={usersNumber} + teamsUsers={teamsUsers} + /> + + )} + + ); +}; + +export default Injects; diff --git a/openbas-front/src/admin/components/common/injects/InjectsListButtons.tsx b/openbas-front/src/admin/components/common/injects/InjectsListButtons.tsx new file mode 100644 index 0000000000..366e0101b6 --- /dev/null +++ b/openbas-front/src/admin/components/common/injects/InjectsListButtons.tsx @@ -0,0 +1,116 @@ +import React, { FunctionComponent, useContext } from 'react'; +import { ToggleButton, ToggleButtonGroup, Tooltip } from '@mui/material'; +import { BarChartOutlined, ReorderOutlined } from '@mui/icons-material'; +import { makeStyles } from '@mui/styles'; +import type { InjectOutputType } from '../../../../actions/injects/Inject'; +import { exportData } from '../../../../utils/Environment'; +import { useFormatter } from '../../../../components/i18n'; +import { InjectContext } from '../Context'; +import ImportUploaderInjectFromXls from './ImportUploaderInjectFromXls'; +import useExportToXLS from '../../../../utils/hooks/useExportToXLS'; +import { useHelper } from '../../../../store'; +import type { TagHelper } from '../../../../actions/helper'; +import ButtonPopover from '../../../../components/common/ButtonPopover'; + +const useStyles = makeStyles(() => ({ + container: { + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + gap: 10, + }, +})); + +interface Props { + injects: InjectOutputType[]; + setViewMode?: (mode: string) => void; + showTimeline: boolean; + handleShowTimeline: () => void; +} + +const InjectsListButtons: FunctionComponent = ({ + injects, + setViewMode, + showTimeline, + handleShowTimeline, +}) => { + // Standard hooks + const classes = useStyles(); + const { t } = useFormatter(); + const injectContext = useContext(InjectContext); + + // Fetching data + const { tagsMap } = useHelper((helper: TagHelper) => ({ + tagsMap: helper.getTagsMap(), + })); + + const isAtLeastOneValidInject = injects.some((inject) => inject.inject_injector_contract?.injector_contract_content_parsed !== null); + + const exportInjects = exportData( + 'inject', + [ + 'inject_type', + 'inject_title', + 'inject_description', + 'inject_depends_duration', + 'inject_enabled', + 'inject_tags', + 'inject_content', + ], + injects, + tagsMap, + ); + const exportInjectsToXLS = useExportToXLS({ data: exportInjects, fileName: `${t('Injects')}` }); + + const onShowTimeline = () => { + handleShowTimeline(); + }; + + const entries = [ + { label: 'Export injects', action: exportInjectsToXLS }, + { label: showTimeline ? t('Hide timeline') : t('Show timeline'), action: onShowTimeline }, + ]; + + return ( +
+ + + {injectContext.onImportInjectFromXls + && } + {!!setViewMode + && + + + + + } + {!!setViewMode + && + setViewMode('distribution')} + aria-label="Distribution view mode" + > + + + + } + +
+ ); +}; + +export default InjectsListButtons; diff --git a/openbas-front/src/admin/components/payloads/Payloads.tsx b/openbas-front/src/admin/components/payloads/Payloads.tsx index fffa271b89..07f50bcb94 100644 --- a/openbas-front/src/admin/components/payloads/Payloads.tsx +++ b/openbas-front/src/admin/components/payloads/Payloads.tsx @@ -26,6 +26,7 @@ import type { CollectorHelper } from '../../../actions/collectors/collector-help import { useAppDispatch } from '../../../utils/hooks'; import type { PayloadStore } from '../../../actions/payloads/Payload'; import { buildEmptyFilter } from '../../../components/common/queryable/filter/FilterUtils'; +import ExportButton from '../../../components/common/ExportButton'; const useStyles = makeStyles(() => ({ itemHead: { @@ -227,7 +228,9 @@ const Payloads = () => { entityPrefix="payload" availableFilterNames={availableFilterNames} queryableHelpers={queryableHelpers} - exportProps={exportProps} + topBarButtons={ + + } /> ({ itemHead: { @@ -202,10 +203,13 @@ const Scenarios = () => { entityPrefix="scenario" availableFilterNames={availableFilterNames} queryableHelpers={queryableHelpers} - exportProps={exportProps} - > - -
+ topBarButtons={ + + + + + } + /> { const dispatch = useAppDispatch(); return { - onAddInject(inject: Inject): Promise<{ result: string }> { + searchInjects(input: SearchPaginationInput): Promise<{ data: Page }> { + return searchScenarioInjectsSimple(scenario.scenario_id, input); + }, + onAddInject(inject: Inject): Promise<{ result: string, entities: { injects: Record } }> { return dispatch(addInjectForScenario(scenario.scenario_id, inject)); }, - onUpdateInject(injectId: Inject['inject_id'], inject: Inject): Promise<{ result: string }> { + onUpdateInject(injectId: Inject['inject_id'], inject: Inject): Promise<{ result: string, entities: { injects: Record } }> { return dispatch(updateInjectForScenario(scenario.scenario_id, injectId, inject)); }, onUpdateInjectActivation(injectId: Inject['inject_id'], injectEnabled: { inject_enabled: boolean }): Promise<{ diff --git a/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx b/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx index 252a8b1b64..5768f206f8 100644 --- a/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx +++ b/openbas-front/src/admin/components/scenarios/scenario/injects/ScenarioInjects.tsx @@ -15,15 +15,15 @@ import useDataLoader from '../../../../../utils/hooks/useDataLoader'; import { fetchVariablesForScenario } from '../../../../../actions/variables/variable-actions'; import { fetchScenarioTeams } from '../../../../../actions/scenarios/scenario-actions'; import type { Inject, InjectStatus, Scenario } from '../../../../../utils/api-types'; -import Injects from '../../../common/injects/Injects'; import { articleContextForScenario } from '../articles/ScenarioArticles'; import { teamContextForScenario } from '../teams/ScenarioTeams'; import useEntityToggle from '../../../../../utils/hooks/useEntityToggle'; import ToolBar from '../../../common/ToolBar'; import { isNotEmptyField } from '../../../../../utils/utils'; import injectContextForScenario from '../ScenarioContext'; -import { fetchScenarioInjectsSimple, bulkTestInjects } from '../../../../../actions/injects/inject-action'; +import { bulkTestInjects, fetchScenarioInjectsSimple } from '../../../../../actions/injects/inject-action'; import { useFormatter } from '../../../../../components/i18n'; +import Injects from '../../../common/injects/Injects'; interface Props { @@ -65,8 +65,8 @@ const ScenarioInjects: FunctionComponent = () => { handleToggleSelectAll, onToggleEntity, numberOfSelectedElements, - } = useEntityToggle('inject', injects.length); - const onRowShiftClick = (currentIndex: number, currentEntity: Inject, event: React.SyntheticEvent | null = null) => { + } = useEntityToggle<{ inject_id: string }>('inject', injects.length); + const onRowShiftClick = (currentIndex: number, currentEntity: { inject_id: string }, event: React.SyntheticEvent | null = null) => { if (event) { event.stopPropagation(); event.preventDefault(); @@ -248,15 +248,14 @@ const ScenarioInjects: FunctionComponent = () => { { // Standard hooks @@ -86,10 +88,13 @@ const Exercises = () => { entityPrefix="exercise" availableFilterNames={availableFilterNames} queryableHelpers={queryableHelpers} - exportProps={exportProps} - > - - + topBarButtons={ + + + + + } + /> { const dispatch = useAppDispatch(); return { - onAddInject(inject: Inject): Promise<{ result: string }> { + searchInjects(input: SearchPaginationInput): Promise<{ data: Page }> { + return searchExerciseInjectsSimple(exercise.exercise_id, input); + }, + onAddInject(inject: Inject): Promise<{ result: string, entities: { injects: Record } }> { return dispatch(addInjectForExercise(exercise.exercise_id, inject)); }, - onUpdateInject(injectId: Inject['inject_id'], inject: Inject): Promise<{ result: string }> { + onUpdateInject(injectId: Inject['inject_id'], inject: Inject): Promise<{ result: string, entities: { injects: Record } }> { return dispatch(updateInjectForExercise(exercise.exercise_id, injectId, inject)); }, onUpdateInjectTrigger(injectId: Inject['inject_id']): void { diff --git a/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx b/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx index df97596538..ade8071b16 100644 --- a/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx +++ b/openbas-front/src/admin/components/simulations/simulation/injects/ExerciseInjects.tsx @@ -9,7 +9,6 @@ import { ArticleContext, TeamContext } from '../../../common/Context'; import { useAppDispatch } from '../../../../../utils/hooks'; import { useHelper } from '../../../../../store'; import useDataLoader from '../../../../../utils/hooks/useDataLoader'; -import Injects from '../../../common/injects/Injects'; import { fetchExerciseInjectExpectations, fetchExerciseTeams } from '../../../../../actions/Exercise'; import type { ExercisesHelper } from '../../../../../actions/exercises/exercise-helper'; import type { ArticlesHelper } from '../../../../../actions/channels/article-helper'; @@ -32,6 +31,7 @@ import ToolBar from '../../../common/ToolBar'; import { isNotEmptyField } from '../../../../../utils/utils'; import { fetchExerciseInjectsSimple, bulkTestInjects } from '../../../../../actions/injects/inject-action'; import injectContextForExercise from '../ExerciseContext'; +import Injects from '../../../common/injects/Injects'; const useStyles = makeStyles(() => ({ paperChart: { @@ -86,8 +86,8 @@ const ExerciseInjects: FunctionComponent = () => { handleToggleSelectAll, onToggleEntity, numberOfSelectedElements, - } = useEntityToggle('inject', injects.length); - const onRowShiftClick = (currentIndex: number, currentEntity: Inject, event: React.SyntheticEvent | null = null) => { + } = useEntityToggle<{ inject_id: string }>('inject', injects.length); + const onRowShiftClick = (currentIndex: number, currentEntity: { inject_id: string }, event: React.SyntheticEvent | null = null) => { if (event) { event.stopPropagation(); event.preventDefault(); @@ -274,6 +274,7 @@ const ExerciseInjects: FunctionComponent = () => { uriVariable={`/admin/exercises/${exerciseId}/definition/variables`} allUsersNumber={exercise.exercise_all_users_number} usersNumber={exercise.exercise_users_number} + // @ts-expect-error typing teamsUsers={exercise.exercise_teams_users} setViewMode={setViewMode} onToggleEntity={onToggleEntity} diff --git a/openbas-front/src/components/ChainedTimeline.tsx b/openbas-front/src/components/ChainedTimeline.tsx index 198cae2820..ae4715c2d3 100644 --- a/openbas-front/src/components/ChainedTimeline.tsx +++ b/openbas-front/src/components/ChainedTimeline.tsx @@ -17,7 +17,7 @@ import { import { Tooltip } from '@mui/material'; import moment from 'moment-timezone'; import { UnfoldLess, UnfoldMore, CropFree } from '@mui/icons-material'; -import type { InjectStore } from '../actions/injects/Inject'; +import type { InjectOutputType } from '../actions/injects/Inject'; import type { Theme } from './Theme'; import nodeTypes from './nodes'; import CustomTimelineBackground from './CustomTimelineBackground'; @@ -32,6 +32,8 @@ import { parseCron } from '../utils/Cron'; import type { TeamsHelper } from '../actions/teams/team-helper'; 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'; const useStyles = makeStyles(() => ({ container: { @@ -50,10 +52,10 @@ const useStyles = makeStyles(() => ({ })); interface Props { - injects: InjectStore[], + injects: InjectOutputType[], exerciseOrScenarioId: string, onConnectInjects(connection: Connection): void, - onSelectedInject(inject?: InjectStore): void, + onSelectedInject(inject?: InjectOutputType): void, openCreateInjectDrawer(data: { inject_depends_duration_days: number, inject_depends_duration_minutes: number, @@ -81,6 +83,8 @@ const ChainedTimelineFlow: FunctionComponent = ({ injects, exerciseOrScen const injectsMap = useHelper((injectHelper: InjectHelper) => injectHelper.getInjectsMap()); const teams = useHelper((teamsHelper: TeamsHelper) => teamsHelper.getTeamsMap()); + const assets = useHelper((endpointHelper: EndpointHelper) => endpointHelper.getEndpointsMap()); + const assetGroups = useHelper((assetGroupsHelper: AssetGroupsHelper) => assetGroupsHelper.getAssetGroupMaps()); const scenario = useHelper((helper: ScenariosHelper) => helper.getScenario(exerciseOrScenarioId)); const exercise = useHelper((helper: ExercisesHelper) => helper.getExercise(exerciseOrScenarioId)); @@ -160,7 +164,7 @@ const ChainedTimelineFlow: FunctionComponent = ({ injects, exerciseOrScen if (injects.length > 0) { const injectsNodes = injects .sort((a, b) => a.inject_depends_duration - b.inject_depends_duration) - .map((inject: InjectStore) => ({ + .map((inject: InjectOutputType) => ({ id: `${inject.inject_id}`, type: 'inject', data: { @@ -175,8 +179,8 @@ const ChainedTimelineFlow: FunctionComponent = ({ injects, exerciseOrScen fixedY: 0, startDate, onSelectedInject, - targets: inject.inject_assets!.map((asset) => asset.asset_name) - .concat(inject.inject_asset_groups!.map((assetGroup) => assetGroup.asset_group_name)) + 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)), exerciseOrScenarioId, }, diff --git a/openbas-front/src/components/common/ButtonPopover.tsx b/openbas-front/src/components/common/ButtonPopover.tsx index f53d661e73..713251455f 100644 --- a/openbas-front/src/components/common/ButtonPopover.tsx +++ b/openbas-front/src/components/common/ButtonPopover.tsx @@ -15,12 +15,14 @@ interface Props { entries: PopoverEntry[]; buttonProps?: ToggleButtonProps; variant?: VariantButtonPopover; + disabled?: boolean; } const ButtonPopover: FunctionComponent = ({ entries, buttonProps, variant = 'toggle', + disabled = false, }) => { // Standard hooks const { t } = useFormatter(); @@ -39,8 +41,9 @@ const ButtonPopover: FunctionComponent = ({ setAnchorEl(ev.currentTarget); }} style={{ ...buttonProps }} + disabled={disabled} > - + } {variant === 'icon' @@ -53,8 +56,9 @@ const ButtonPopover: FunctionComponent = ({ setAnchorEl(ev.currentTarget); }} style={{ ...buttonProps }} + disabled={disabled} > - + } ({ + topbar: { + display: 'flex', + alignItems: 'center', + }, parameters: { marginTop: -10, display: 'flex', @@ -37,13 +40,12 @@ interface Props { fetch: (input: SearchPaginationInput) => Promise<{ data: Page }>; searchPaginationInput: SearchPaginationInput; setContent: (data: T[]) => void; - exportProps?: ExportProps; searchEnable?: boolean; disablePagination?: boolean; entityPrefix?: string; availableFilterNames?: string[]; queryableHelpers: QueryableHelpers; - children?: React.ReactElement | null; + topBarButtons?: React.ReactElement | null; attackPatterns?: AttackPatternStore[], } @@ -51,14 +53,13 @@ const PaginationComponentV2 = ({ fetch, searchPaginationInput, setContent, - exportProps, searchEnable = true, disablePagination, entityPrefix, availableFilterNames = [], queryableHelpers, attackPatterns, - children, + topBarButtons, }: Props) => { // Standard hooks const classes = useStyles(); @@ -109,6 +110,12 @@ const PaginationComponentV2 = ({ ); }; + // TopBarChildren + let topBarButtonComponent; + if (topBarButtons) { + topBarButtonComponent = React.cloneElement(topBarButtons as React.ReactElement); + } + return ( <>
@@ -145,20 +152,20 @@ const PaginationComponentV2 = ({ )} {availableFilterNames?.includes('injector_contract_players') && (
- +
)}
- {!disablePagination && ( - - {children} - - )} +
+ {!disablePagination && ( + + )} + {!!topBarButtonComponent && topBarButtonComponent} +
{/* Handle Mitre Filter */} {queryableHelpers.filterHelpers && searchPaginationInput.filterGroup && ( diff --git a/openbas-front/src/components/common/queryable/pagination/TablePaginationComponentV2.tsx b/openbas-front/src/components/common/queryable/pagination/TablePaginationComponentV2.tsx new file mode 100644 index 0000000000..cc60dfa9fe --- /dev/null +++ b/openbas-front/src/components/common/queryable/pagination/TablePaginationComponentV2.tsx @@ -0,0 +1,39 @@ +import { TablePagination } from '@mui/material'; +import React, { FunctionComponent } from 'react'; +import { ROWS_PER_PAGE_OPTIONS } from './usPaginationState'; +import { PaginationHelpers } from './PaginationHelpers'; + +interface Props { + page: number; + size: number; + paginationHelpers: PaginationHelpers; +} + +const TablePaginationComponentV2: FunctionComponent = ({ + page, + size, + paginationHelpers, +}) => { + const handleChangePage = ( + _event: React.MouseEvent | null, + newPage: number, + ) => paginationHelpers.handleChangePage(newPage); + + const handleChangeRowsPerPage = ( + event: React.ChangeEvent, + ) => paginationHelpers.handleChangeRowsPerPage(parseInt(event.target.value, 10)); + + return ( + + ); +}; + +export default TablePaginationComponentV2; diff --git a/openbas-front/src/components/nodes/NodeInject.tsx b/openbas-front/src/components/nodes/NodeInject.tsx index fff853e15c..fec6161c3e 100644 --- a/openbas-front/src/components/nodes/NodeInject.tsx +++ b/openbas-front/src/components/nodes/NodeInject.tsx @@ -7,7 +7,7 @@ import type { Theme } from '../Theme'; import { isNotEmptyField } from '../../utils/utils'; import InjectIcon from '../../admin/components/common/injects/InjectIcon'; import InjectPopover from '../../admin/components/common/injects/InjectPopover'; -import type { InjectStore } from '../../actions/injects/Inject'; +import type { InjectOutputType } from '../../actions/injects/Inject'; // Deprecated - https://mui.com/system/styles/basics/ // Do not use it for new code. @@ -82,12 +82,12 @@ export type NodeInject = Node<{ isTargeted?: boolean, isTargeting?: boolean, onConnectInjects?: OnConnect - inject?: InjectStore, + inject?: InjectOutputType, fixedY?: number, startDate?: string, targets: string[], exerciseOrScenarioId: string, - onSelectedInject(inject?: InjectStore): void, + onSelectedInject(inject?: InjectOutputType): void, } >; diff --git a/openbas-front/src/utils/api-types.d.ts b/openbas-front/src/utils/api-types.d.ts index 63279c4e8b..e6355061a3 100644 --- a/openbas-front/src/utils/api-types.d.ts +++ b/openbas-front/src/utils/api-types.d.ts @@ -741,7 +741,7 @@ export interface Exercise { exercise_scenario?: Scenario; /** @format double */ exercise_score?: number; - exercise_severity?: string; + exercise_severity?: "low" | "medium" | "high" | "critical"; /** @format date-time */ exercise_start_date?: string; exercise_status: "SCHEDULED" | "CANCELED" | "RUNNING" | "PAUSED" | "FINISHED"; @@ -1085,7 +1085,7 @@ export interface Inject { * @min 0 */ inject_depends_duration: number; - inject_depends_on?: string; + inject_depends_on?: Inject; inject_description?: string; inject_documents?: InjectDocument[]; inject_enabled?: boolean; @@ -1103,7 +1103,6 @@ export interface Inject { /** @uniqueItems true */ inject_tags?: Tag[]; inject_teams?: Team[]; - inject_test_status?: InjectTestStatus; inject_testable?: boolean; inject_title: string; inject_type?: string; @@ -1251,6 +1250,7 @@ export interface InjectOutput { * @min 0 */ inject_depends_duration: number; + inject_depends_on?: string; inject_enabled?: boolean; inject_exercise?: string; inject_id: string; @@ -1261,7 +1261,7 @@ export interface InjectOutput { inject_tags?: string[]; inject_teams?: string[]; inject_testable?: boolean; - inject_title?: string; + inject_title: string; inject_type?: string; } @@ -2637,7 +2637,7 @@ export interface RawPaginationScenario { /** @uniqueItems true */ scenario_platforms?: string[]; scenario_recurrence?: string; - scenario_severity?: string; + scenario_severity?: "low" | "medium" | "high" | "critical"; /** @uniqueItems true */ scenario_tags?: string[]; /** @format date-time */ @@ -2798,7 +2798,7 @@ export interface Scenario { scenario_recurrence_end?: string; /** @format date-time */ scenario_recurrence_start?: string; - scenario_severity?: string; + scenario_severity?: "low" | "medium" | "high" | "critical"; scenario_subtitle?: string; /** @uniqueItems true */ scenario_tags?: Tag[]; diff --git a/openbas-front/vite.config.ts b/openbas-front/vite.config.ts index 7e998d1cda..62b3749ca4 100644 --- a/openbas-front/vite.config.ts +++ b/openbas-front/vite.config.ts @@ -81,6 +81,7 @@ export default ({ mode }: { mode: string }) => { 'uuid', 'xlsx', 'zod', + 'zustand/shallow', ], }, 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 c7481412f6..c051fb7933 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 @@ -129,12 +129,13 @@ public class Inject implements Base, Injection { @JsonProperty("inject_depends_duration") @NotNull @Min(value = 0L, message = "The value must be positive") + @Queryable(sortable = true) private Long dependsDuration; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "inject_injector_contract") @JsonProperty("inject_injector_contract") - @Queryable(filterable = true, path = "injectorContract.injector.id") + @Queryable(filterable = true, dynamicValues = true, path = "injectorContract.injector.id") private InjectorContract injectorContract; @Getter @@ -376,10 +377,11 @@ private String getType() { @JsonIgnore @JsonProperty("inject_platforms") - @Queryable(filterable = true, dynamicValues = true, path = "injectorContract.platforms", clazz = String[].class) - private Optional getPlatforms() { + @Queryable(filterable = true, path = "injectorContract.platforms", clazz = String[].class) + private Endpoint.PLATFORM_TYPE[] getPlatforms() { return getInjectorContract() - .map(InjectorContract::getPlatforms); + .map(InjectorContract::getPlatforms) + .orElse(new Endpoint.PLATFORM_TYPE[0]); } @JsonIgnore